├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── priv └── index.html ├── rebar.config ├── rebar.lock ├── src ├── prometheus_http_config.erl ├── prometheus_http_impl.erl ├── prometheus_httpd.app.src └── prometheus_httpd.erl └── test ├── prometheus_http_ct_test.exs ├── prometheus_httpd_SUITE.erl ├── prometheus_httpd_ct.erl └── test_helper.exs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "*" 10 | workflow_dispatch: {} 11 | 12 | jobs: 13 | test: 14 | name: OTP ${{matrix.otp}} 15 | runs-on: 'ubuntu-24.04' 16 | strategy: 17 | matrix: 18 | otp: ['27', '26', '25'] 19 | rebar3: ['3.24.0'] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: erlef/setup-beam@v1 23 | with: 24 | otp-version: ${{matrix.otp}} 25 | rebar3-version: ${{matrix.rebar3}} 26 | - run: rebar3 compile 27 | - run: rebar3 lint 28 | - run: rebar3 fmt --check 29 | if: ${{ matrix.otp == '27' }} 30 | - run: rebar3 dialyzer 31 | if: ${{ matrix.otp == '27' }} 32 | - run: rebar3 ex_doc 33 | - run: rebar3 xref 34 | - run: rebar3 eunit 35 | - run: rebar3 ct 36 | - run: rebar3 do cover, covertool generate 37 | - name: Upload code coverage 38 | uses: codecov/codecov-action@v5 39 | with: 40 | files: _build/test/covertool/*.covertool.xml 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | fail_ci_if_error: true 43 | verbose: true 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | rebar3.crashdump 3 | _* 4 | .eunit 5 | *.o 6 | *.beam 7 | *.plt 8 | *.swp 9 | *.swo 10 | *~ 11 | \#* 12 | .#* 13 | .erlang.cookie 14 | ebin 15 | log 16 | erl_crash.dump 17 | .rebar 18 | logs 19 | _build 20 | deps 21 | *.ez 22 | doc/ 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017- Ilya Khaprov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus.io inets httpd exporter # 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/prometheus_httpd.svg?maxAge=2592000?style=plastic)](https://hex.pm/packages/prometheus_httpd) 4 | [![Hex.pm](https://img.shields.io/hexpm/dt/prometheus_httpd.svg?maxAge=2592000)](https://hex.pm/packages/prometheus_httpd) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/prometheus_httpd/) 6 | [![GitHub Actions](https://github.com/prometheus-erl/prometheus-httpd/actions/workflows/ci.yml/badge.svg)](https://github.com/prometheus-erl/prometheus-httpd/actions/workflows/ci.yml) 7 | [![Codecov](https://codecov.io/github/prometheus-erl/prometheus-httpd/graph/badge.svg?token=G9HB5UKNIY)](https://codecov.io/github/prometheus-erl/prometheus-httpd) 8 | 9 | Provides [httpd middleware](http://erlang.org/doc/man/httpd.html) "mod-module" (`prometheus_httpd`) for exposing [Prometheus.io](https://github.com/prometheus-erl/prometheus.erl) metrics in various formats. 10 | 11 | Also can start its own httpd instance with just `prometheus_httpd` enabled. 12 | 13 | ## Usage 14 | 15 | ``` 16 | prometheus_httpd:start() 17 | ``` 18 | 19 | More in the `prometheus_httpd` module [documentation](https://hexdocs.pm/prometheus_httpd/prometheus_httpd.md) 20 | 21 | ![BEAM Dashboard](https://raw.githubusercontent.com/prometheus-erl/beam-dashboards/master/BEAM.png) 22 | 23 | - IRC: #erlang on Freenode; 24 | - [Slack](https://elixir-slackin.herokuapp.com/): #prometheus channel - [Browser](https://elixir-lang.slack.com/messages/prometheus) or App(slack://elixir-lang.slack.com/messages/prometheus). 25 | 26 | ## Integrations 27 | - [Ecto Instrumenter](https://hex.pm/packages/prometheus_ecto) 28 | - [Erlang client](https://github.com/prometheus-erl/prometheus.erl) 29 | - [Elixir client](https://github.com/prometheus-erl/prometheus.ex) 30 | - [Elixir plugs Instrumenters and Exporter](https://hex.pm/packages/prometheus_plugs) 31 | - [Extatus - App to report metrics to Prometheus from Elixir GenServers](https://github.com/gmtprime/extatus) 32 | - [Fuse plugin](https://github.com/jlouis/fuse#fuse_stats_prometheus) 33 | - [OS process info Collector](https://hex.pm/packages/prometheus_process_collector) (linux-only) 34 | - [Phoenix Instrumenter](https://hex.pm/packages/prometheus_phoenix) 35 | - [RabbitMQ Exporter](https://github.com/prometheus-erl/prometheus_rabbitmq_exporter). 36 | 37 | ## Dashboards 38 | 39 | - [Beam Dashboards](https://github.com/prometheus-erl/beam-dashboards). 40 | 41 | ## Blogs 42 | 43 | - [Monitoring Elixir apps in 2016: Prometheus and Grafana](https://aldusleaf.org/monitoring-elixir-apps-in-2016-prometheus-and-grafana/) 44 | - [A Simple Erlang Application, with Prometheus](http://markbucciarelli.com/2016-11-23_a_simple_erlang_application_with_prometheus.html). 45 | -------------------------------------------------------------------------------- /priv/index.html: -------------------------------------------------------------------------------- 1 | 2 | Prometheus Exporter 3 | 4 |
5 |

Welcome to prometheus exporter.

6 |

Metrics Endpoint

7 | 8 | 20 | 21 | 28 | 29 |

Integrations

30 | 41 | 42 |

Dashboards

43 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | warn_unused_vars, 4 | warnings_as_errors, 5 | warn_export_all, 6 | warn_shadow_vars, 7 | warn_unused_import, 8 | warn_unused_function, 9 | warn_bif_clash, 10 | warn_unused_record, 11 | warn_deprecated_function, 12 | warn_obsolete_guard, 13 | strict_validation, 14 | warn_export_vars, 15 | warn_exported_vars, 16 | warn_missing_spec, 17 | warn_missing_doc 18 | ]}. 19 | 20 | {deps, [ 21 | {prometheus, "~> 5.0"}, 22 | {accept, "~> 0.3"} 23 | ]}. 24 | 25 | {shell, [{apps, [prometheus_httpd]}]}. 26 | 27 | {xref_extra_paths, []}. 28 | {xref_checks, [ 29 | undefined_function_calls, 30 | undefined_functions, 31 | locals_not_used, 32 | deprecated_function_calls, 33 | deprecated_functions 34 | ]}. 35 | 36 | {profiles, [ 37 | {test, [ 38 | {erl_opts, [nowarn_export_all, nowarn_missing_spec, nowarn_missing_doc]}, 39 | {covertool, [{coverdata_files, ["eunit.coverdata", "ct.coverdata"]}]}, 40 | {cover_enabled, true}, 41 | {cover_export_enabled, true} 42 | ]} 43 | ]}. 44 | 45 | {project_plugins, [ 46 | {rebar3_hex, "~> 7.0"}, 47 | {rebar3_lint, "~> 4.0"}, 48 | {rebar3_ex_doc, "~> 0.2"}, 49 | {erlfmt, "~> 1.6"}, 50 | {covertool, "~> 2.0"} 51 | ]}. 52 | 53 | {hex, [{doc, #{provider => ex_doc}}]}. 54 | 55 | {ex_doc, [ 56 | {source_url, <<"https://github.com/prometheus-erl/prometheus-httpd">>}, 57 | {main, <<"readme">>}, 58 | {extras, [ 59 | {'README.md', #{title => <<"Overview">>}}, 60 | {'LICENSE', #{title => <<"License">>}} 61 | ]} 62 | ]}. 63 | 64 | {erlfmt, [ 65 | write, 66 | {files, [ 67 | "include/**/*.{hrl,erl,app.src}", 68 | "src/**/*.{hrl,erl,app.src}", 69 | "test/**/*.{hrl,erl,app.src}", 70 | "rebar.config" 71 | ]} 72 | ]}. 73 | 74 | {elvis, [ 75 | #{ 76 | dirs => ["src"], 77 | filter => "*.erl", 78 | rules => [ 79 | {elvis_text_style, line_length, #{limit => 100}}, 80 | {elvis_style, invalid_dynamic_call, #{ 81 | ignore => [ 82 | prometheus_http_impl, 83 | prometheus_httpd_config 84 | ] 85 | }}, 86 | {elvis_style, atom_naming_convention, #{ 87 | regex => "^([a-z][a-zA-Z0-9_]*_?)*(_SUITE)?$" 88 | }}, 89 | {elvis_style, function_naming_convention, #{ 90 | regex => "^([a-z][a-zA-Z0-9_]*_?)*(_SUITE)?$" 91 | }}, 92 | {elvis_style, nesting_level, #{level => 5}}, 93 | {elvis_style, god_modules, #{limit => 40}}, 94 | {elvis_style, dont_repeat_yourself, #{min_complexity => 20}} 95 | ], 96 | ruleset => erl_files 97 | }, 98 | #{ 99 | dirs => ["test/"], 100 | filter => "*.erl", 101 | rules => [ 102 | {elvis_text_style, line_length, #{limit => 100}}, 103 | {elvis_style, no_debug_call, #{ 104 | ignore => [], 105 | debug_functions => [] 106 | }}, 107 | {elvis_style, invalid_dynamic_call, #{ignore => []}}, 108 | {elvis_style, god_modules, #{limit => 40}}, 109 | %% looks like eunit generates underscored vars 110 | {elvis_style, variable_naming_convention, #{regex => "^([A-Z_][0-9a-zA-Z_]*)$"}}, 111 | {elvis_style, dont_repeat_yourself, #{min_complexity => 200}} 112 | ], 113 | ruleset => erl_files 114 | }, 115 | #{ 116 | dirs => ["."], 117 | filter => "rebar.config", 118 | ruleset => rebar_config 119 | } 120 | ]}. 121 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"accept">>,{pkg,<<"accept">>,<<"0.3.7">>},0}, 3 | {<<"prometheus">>,{pkg,<<"prometheus">>,<<"5.0.0">>},0}, 4 | {<<"quantile_estimator">>,{pkg,<<"quantile_estimator">>,<<"1.0.2">>},1}]}. 5 | [ 6 | {pkg_hash,[ 7 | {<<"accept">>, <<"CD6E34A2D7E28CA38B2D3CB233734CA0C221EFBC1F171F91FEC5F162CC2D18DA">>}, 8 | {<<"prometheus">>, <<"8A37A3216D8DB019D19068602669C9819C099120F8E39994DD1BD3A3F5553376">>}, 9 | {<<"quantile_estimator">>, <<"ECD281D40110FDD9BA62685531E4435E0839A52FD1058DA5564F1763E4642EF7">>}]}, 10 | {pkg_hash_ext,[ 11 | {<<"accept">>, <<"CA69388943F5DAD2E7232A5478F16086E3C872F48E32B88B378E1885A59F5649">>}, 12 | {<<"prometheus">>, <<"80D29564A5DC4490B53FD225D752B65FB0DBEBA41497F96D62223338127C5659">>}, 13 | {<<"quantile_estimator">>, <<"DB404793D6384995A1AC6DD973E2CEE5BE9FCC128765BDBA53D87C564E296B64">>}]} 14 | ]. 15 | -------------------------------------------------------------------------------- /src/prometheus_http_config.erl: -------------------------------------------------------------------------------- 1 | -module(prometheus_http_config). 2 | -if(?OTP_RELEASE >= 27). 3 | -define(MODULEDOC(Str), -moduledoc(Str)). 4 | -else. 5 | -define(MODULEDOC(Str), -compile([])). 6 | -endif. 7 | ?MODULEDOC(false). 8 | 9 | -export([ 10 | path/0, 11 | valid_path_and_registry/2, 12 | format/0, 13 | allowed_formats/0, 14 | registry/0, 15 | telemetry_registry/0, 16 | port/0, 17 | authorization/0 18 | ]). 19 | 20 | %% Macros. 21 | -define(DEFAULT_PATH, "/metrics"). 22 | -define(DEFAULT_FORMAT, auto). 23 | -define(DEFAULT_REGISTRY, auto). 24 | -define(DEFAULT_TELEMETRY_REGISTRY, default). 25 | -define(DEFAULT_AUTHORIZATION, false). 26 | -define(DEFAULT_PORT, 8081). 27 | 28 | -define(DEFAULT_CONFIG, [ 29 | {path, ?DEFAULT_PATH}, 30 | {format, ?DEFAULT_FORMAT}, 31 | {registry, ?DEFAULT_REGISTRY}, 32 | {telemetry_registry, ?DEFAULT_TELEMETRY_REGISTRY}, 33 | {port, ?DEFAULT_PORT}, 34 | {authorization, ?DEFAULT_AUTHORIZATION} 35 | ]). 36 | 37 | %%%=================================================================== 38 | %%% API 39 | %%%=================================================================== 40 | 41 | -spec path() -> string(). 42 | path() -> 43 | get_value(path, ?DEFAULT_PATH). 44 | 45 | -spec valid_path_and_registry(true | string(), prometheus_registry:registry()) -> 46 | false 47 | | {true, prometheus_registry:registry()} 48 | | {registry_conflict, prometheus_registry:registry(), prometheus_registry:registry()} 49 | | {registry_not_found, term()}. 50 | valid_path_and_registry(URI, RegistryO) -> 51 | case try_match_path(path(), URI) of 52 | false -> 53 | false; 54 | undefined -> 55 | validate_registry(RegistryO, registry()); 56 | Registry0 -> 57 | case prometheus_registry:exists(Registry0) of 58 | false -> 59 | {registry_not_found, Registry0}; 60 | Registry -> 61 | validate_registry(Registry, registry()) 62 | end 63 | end. 64 | 65 | -spec registry() -> prometheus_registry:registry() | auto. 66 | registry() -> 67 | get_value(registry, ?DEFAULT_REGISTRY). 68 | 69 | -spec telemetry_registry() -> prometheus_registry:registry() | default. 70 | telemetry_registry() -> 71 | get_value(telemetry_registry, ?DEFAULT_TELEMETRY_REGISTRY). 72 | 73 | -spec format() -> atom(). 74 | format() -> 75 | get_value(format, ?DEFAULT_FORMAT). 76 | 77 | -spec allowed_formats() -> [{binary(), module()}]. 78 | allowed_formats() -> 79 | [ 80 | {prometheus_text_format:content_type(), prometheus_text_format}, 81 | {prometheus_protobuf_format:content_type(), prometheus_protobuf_format} 82 | ]. 83 | 84 | -spec port() -> inet:port_number(). 85 | port() -> 86 | get_value(port, ?DEFAULT_PORT). 87 | 88 | -spec authorization() -> fun((#{headers := _, _ => _}) -> boolean()) | {invalid_authorize, term()}. 89 | authorization() -> 90 | case get_value(authorization, ?DEFAULT_AUTHORIZATION) of 91 | false -> 92 | fun(_) -> 93 | true 94 | end; 95 | {basic, Login, Password} -> 96 | fun(#{headers := Headers}) -> 97 | call_with_basic_auth( 98 | Headers, 99 | fun(Login1, Password1) -> 100 | case {Login1, Password1} of 101 | {Login, Password} -> 102 | true; 103 | _ -> 104 | false 105 | end 106 | end 107 | ) 108 | end; 109 | {basic, {Module, Fun}} when 110 | is_atom(Module) andalso is_atom(Fun) 111 | -> 112 | fun(#{headers := Headers}) -> 113 | call_with_basic_auth( 114 | Headers, 115 | fun Module:Fun/2 116 | ) 117 | end; 118 | {basic, Module} when is_atom(Module) -> 119 | fun(#{headers := Headers}) -> 120 | call_with_basic_auth( 121 | Headers, 122 | fun Module:authorize/2 123 | ) 124 | end; 125 | {Module, Fun} when 126 | is_atom(Module) andalso is_atom(Fun) 127 | -> 128 | fun Module:Fun/1; 129 | Module when is_atom(Module) -> 130 | fun Module:authorize/1; 131 | C -> 132 | {invalid_authorize, C} 133 | end. 134 | 135 | %%%=================================================================== 136 | %%% Private functions 137 | %%%=================================================================== 138 | 139 | validate_registry(undefined, auto) -> 140 | {true, default}; 141 | validate_registry(Registry, auto) -> 142 | {true, Registry}; 143 | validate_registry(Registry, Registry) -> 144 | {true, Registry}; 145 | validate_registry(Asked, Conf) -> 146 | {registry_conflict, Asked, Conf}. 147 | 148 | try_match_path(_, true) -> 149 | undefined; 150 | try_match_path(Path, Path) -> 151 | undefined; 152 | try_match_path(Path, URI) -> 153 | PS = Path ++ "/", 154 | case lists:prefix(PS, URI) of 155 | true -> 156 | lists:sublist(URI, length(PS) + 1, length(URI)); 157 | false -> 158 | false 159 | end. 160 | 161 | get_value(Key, Default) -> 162 | proplists:get_value(Key, config(), Default). 163 | 164 | config() -> 165 | application:get_env(prometheus, prometheus_http, ?DEFAULT_CONFIG). 166 | 167 | call_with_basic_auth(Headers, Fun) -> 168 | Authorization = Headers("authorization", undefined), 169 | call_with_basic_auth_(Authorization, Fun). 170 | 171 | call_with_basic_auth_("Basic " ++ Encoded, Fun) -> 172 | call_with_basic_auth__(Encoded, Fun); 173 | call_with_basic_auth_(<<"Basic ", Encoded/binary>>, Fun) -> 174 | call_with_basic_auth__(Encoded, Fun); 175 | call_with_basic_auth_(_Authorization, _Fun) -> 176 | false. 177 | 178 | call_with_basic_auth__(Encoded, Fun) -> 179 | Params = base64:decode_to_string(Encoded), 180 | case string:tokens(Params, ":") of 181 | [Login, Password] -> 182 | Fun(Login, Password); 183 | _ -> 184 | false 185 | end. 186 | -------------------------------------------------------------------------------- /src/prometheus_http_impl.erl: -------------------------------------------------------------------------------- 1 | -module(prometheus_http_impl). 2 | -if(?OTP_RELEASE >= 27). 3 | -define(MODULEDOC(Str), -moduledoc(Str)). 4 | -define(DOC(Str), -doc(Str)). 5 | -else. 6 | -define(MODULEDOC(Str), -compile([])). 7 | -define(DOC(Str), -compile([])). 8 | -endif. 9 | ?MODULEDOC("Internal module for `prometheus_httpd`."). 10 | 11 | -export([reply/1, setup/0]). 12 | 13 | -define(SCRAPE_DURATION, telemetry_scrape_duration_seconds). 14 | -define(SCRAPE_SIZE, telemetry_scrape_size_bytes). 15 | -define(SCRAPE_ENCODED_SIZE, telemetry_scrape_encoded_size_bytes). 16 | 17 | ?DOC("Renders metrics."). 18 | -spec reply(#{ 19 | path => true | string(), 20 | headers => fun((string(), string()) -> string()), 21 | registry => prometheus_registry:registry(), 22 | standalone => boolean() 23 | }) -> {integer(), list(), iodata()} | false. 24 | reply(#{ 25 | path := Path, 26 | headers := Headers, 27 | registry := Registry, 28 | standalone := Standalone 29 | }) -> 30 | case prometheus_http_config:valid_path_and_registry(Path, Registry) of 31 | {true, RealRegistry} -> 32 | if_authorized( 33 | Path, 34 | Headers, 35 | fun() -> 36 | format_metrics(Headers, RealRegistry) 37 | end 38 | ); 39 | {registry_conflict, _ReqR, _ConfR} -> 40 | {409, [], <<>>}; 41 | {registry_not_found, _ReqR} -> 42 | {404, [], <<>>}; 43 | false -> 44 | maybe_render_index(Standalone, Path, Headers) 45 | end. 46 | 47 | ?DOC(""" 48 | Initializes telemetry metrics. 49 | 50 | *NOTE:* If you plug `prometheus_httpd` in your existing httpd instance, 51 | you have to call this function manually. 52 | """). 53 | -spec setup() -> any(). 54 | setup() -> 55 | TelemetryRegistry = prometheus_http_config:telemetry_registry(), 56 | 57 | ScrapeDuration = [ 58 | {name, ?SCRAPE_DURATION}, 59 | {help, "Scrape duration"}, 60 | {labels, ["registry", "content_type"]}, 61 | {registry, TelemetryRegistry} 62 | ], 63 | ScrapeSize = [ 64 | {name, ?SCRAPE_SIZE}, 65 | {help, "Scrape size, not encoded"}, 66 | {labels, ["registry", "content_type"]}, 67 | {registry, TelemetryRegistry} 68 | ], 69 | ScrapeEncodedSize = [ 70 | {name, ?SCRAPE_ENCODED_SIZE}, 71 | {help, "Scrape size, encoded"}, 72 | {labels, ["registry", "content_type", "encoding"]}, 73 | {registry, TelemetryRegistry} 74 | ], 75 | 76 | prometheus_summary:declare(ScrapeDuration), 77 | prometheus_summary:declare(ScrapeSize), 78 | prometheus_summary:declare(ScrapeEncodedSize). 79 | 80 | %% =================================================================== 81 | %% Private Parts 82 | %% =================================================================== 83 | 84 | format_metrics(Headers, Registry) -> 85 | Accept = Headers("accept", "text/plain"), 86 | AcceptEncoding = Headers("accept-encoding", undefined), 87 | format_metrics(Accept, AcceptEncoding, Registry). 88 | 89 | maybe_render_index(Standalone, Path, Headers) -> 90 | case Standalone of 91 | true -> 92 | case Path of 93 | "/favicon.ico" -> 94 | %% https://www.iconfinder.com/icons/85652/fire_torch_icon#size=30 95 | %% http://www.iconbeast.com/ 96 | {200, [{"content-type", "image/png"}], 97 | base64:decode( 98 | "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAABaklEQ" 99 | "VRIS+2UsU0EQQxF3+UElAASBUAHUAACKgACcqjg6AByAqACOFEAJU" 100 | "ABBHRAQg760liaG+2u7QF0CROd9sb/+dsez+g7G8AzcAE89kjMeoK" 101 | "AbeClxF6XBFJSveAH4LAipXXSAcAVcN7YS+tkA26Bk4GaSmcXOIiW" 102 | "PQMegyoP6Vj5d4BXr+FR8CUwnxC7qyqh36e/Bf4A1j2xzLBFHX8NQ" 103 | "N/LN73p9ri67oWi2IIFVS/VVw3Vn4G1pWqAemh91dDVR0ltem2JOl" 104 | "Y5tamsz3VcO+1HkTUaBcuA1qScC17HqRL6rmOV8AwvCbiXC1QOF6X" 105 | "UitFCOS6Lw32/Bsk4jiQWvhMFWymjwnvexSjY00n/nwFHXbtulWUG" 106 | "/ASsOdY+gf2I/QzYpndK976a9kl+BiyhG2BrRPENOIu4zZbaNIech" 107 | "53+9B23gxYaqLoa2VJb7MrASsDgabe9PW5d/4NDL6p3uFba45CzsU" 108 | "vfrgozHx5iraMAAAAASUVORK5CYII=" 109 | )}; 110 | _ -> 111 | MetricsPath = prometheus_http_config:path(), 112 | if_authorized( 113 | Path, 114 | Headers, 115 | fun() -> 116 | {200, [], prepare_index(MetricsPath)} 117 | end 118 | ) 119 | end; 120 | false -> 121 | false 122 | end. 123 | 124 | if_authorized(URI, Headers, Fun) -> 125 | case prometheus_http_config:authorization() of 126 | {invalid_authorize, _} -> 127 | {500, [], <<>>}; 128 | Auth -> 129 | case 130 | Auth(#{ 131 | uri => URI, 132 | headers => Headers 133 | }) 134 | of 135 | true -> 136 | Fun(); 137 | false -> 138 | {403, [], <<>>} 139 | end 140 | end. 141 | 142 | prepare_index(Path) -> 143 | FileName = filename:join([code:priv_dir(prometheus_httpd), "index.html"]), 144 | {ok, Content} = file:read_file(FileName), 145 | re:replace(Content, "M_E_T_R_I_C_S", Path, [global, {return, list}]). 146 | 147 | format_metrics(Accept, AcceptEncoding, Registry) -> 148 | case negotiate_format(Accept) of 149 | undefined -> 150 | {406, [], <<>>}; 151 | Format -> 152 | {ContentType, Scrape} = render_format(Format, Registry), 153 | case negotiate_encoding(AcceptEncoding) of 154 | undefined -> 155 | {406, [], <<>>}; 156 | Encoding -> 157 | encode_format(ContentType, binary_to_list(Encoding), Scrape, Registry) 158 | end 159 | end. 160 | 161 | negotiate_format(Accept) -> 162 | case prometheus_http_config:format() of 163 | auto -> 164 | Alternatives = prometheus_http_config:allowed_formats(), 165 | accept_header:negotiate(Accept, Alternatives); 166 | Format0 -> 167 | Format0 168 | end. 169 | 170 | negotiate_encoding(AcceptEncoding) -> 171 | %% curl and other tools do not send Accept-Encoding field. 172 | %% RFC says it means we can render any encoding. 173 | %% But people want to see something meaningful in console! 174 | accept_encoding_header:negotiate(AcceptEncoding, [ 175 | <<"identity">>, 176 | <<"gzip">>, 177 | <<"deflate">> 178 | ]). 179 | 180 | render_format(Format, Registry) -> 181 | ContentType = Format:content_type(), 182 | TelemetryRegistry = prometheus_http_config:telemetry_registry(), 183 | 184 | Scrape = prometheus_summary:observe_duration( 185 | TelemetryRegistry, 186 | ?SCRAPE_DURATION, 187 | [Registry, ContentType], 188 | fun() -> Format:format(Registry) end 189 | ), 190 | prometheus_summary:observe( 191 | TelemetryRegistry, 192 | ?SCRAPE_SIZE, 193 | [Registry, ContentType], 194 | iolist_size(Scrape) 195 | ), 196 | {ContentType, Scrape}. 197 | 198 | encode_format(ContentType, Encoding, Scrape, Registry) -> 199 | Encoded = encode_format_(Encoding, Scrape), 200 | TelemetryRegistry = prometheus_http_config:telemetry_registry(), 201 | 202 | prometheus_summary:observe( 203 | TelemetryRegistry, 204 | ?SCRAPE_ENCODED_SIZE, 205 | [Registry, ContentType, Encoding], 206 | iolist_size(Encoded) 207 | ), 208 | {200, 209 | [ 210 | {content_type, binary_to_list(ContentType)}, 211 | {content_encoding, Encoding} 212 | ], 213 | Encoded}. 214 | 215 | encode_format_("gzip", Scrape) -> 216 | zlib:gzip(Scrape); 217 | encode_format_("deflate", Scrape) -> 218 | ZStream = zlib:open(), 219 | zlib:deflateInit(ZStream), 220 | try 221 | zlib:deflate(ZStream, Scrape, finish) 222 | after 223 | zlib:deflateEnd(ZStream) 224 | end; 225 | encode_format_("identity", Scrape) -> 226 | Scrape. 227 | -------------------------------------------------------------------------------- /src/prometheus_httpd.app.src: -------------------------------------------------------------------------------- 1 | {application, prometheus_httpd, [ 2 | {description, "Prometheus.io inets httpd exporter"}, 3 | {vsn, git}, 4 | {registered, []}, 5 | {applications, [ 6 | kernel, 7 | stdlib, 8 | inets, 9 | prometheus, 10 | accept 11 | ]}, 12 | {env, []}, 13 | {modules, []}, 14 | {mod, {prometheus_httpd, []}}, 15 | {description, "Expose Prometheus metrics using inets httpd."}, 16 | {licenses, ["MIT"]}, 17 | {links, [ 18 | {"Github", "https://github.com/prometheus-erl/prometheus-httpd"}, 19 | {"Hex.pm", "https://hex.pm/packages/prometheus_httpd"}, 20 | {"Beam Dashboards", "https://github.com/prometheus-erl/beam-dashboards"}, 21 | {"Prometheus", "https://hex.pm/packages/prometheus"}, 22 | {"Prometheus.ex", "https://hex.pm/packages/prometheus_ex"}, 23 | {"Pushgateway client", "https://hex.pm/packages/prometheus_push"}, 24 | {"Ecto Instrumenter", "https://hex.pm/packages/prometheus_ecto"}, 25 | {"Phoenix Instrumenter", "https://hex.pm/packages/prometheus_phoenix"}, 26 | {"Plugs Instrumenter/Exporter", "https://hex.pm/packages/prometheus_plugs"}, 27 | {"Process info Collector", "https://hex.pm/packages/prometheus_process_collector"} 28 | ]} 29 | ]}. 30 | -------------------------------------------------------------------------------- /src/prometheus_httpd.erl: -------------------------------------------------------------------------------- 1 | -module(prometheus_httpd). 2 | -if(?OTP_RELEASE >= 27). 3 | -define(MODULEDOC(Str), -moduledoc(Str)). 4 | -define(DOC(Str), -doc(Str)). 5 | -else. 6 | -define(MODULEDOC(Str), -compile([])). 7 | -define(DOC(Str), -compile([])). 8 | -endif. 9 | ?MODULEDOC(""" 10 | Exports Prometheus metrics via configurable endpoint. 11 | 12 | ### Existing httpd: 13 | ``` 14 | {modules, [ 15 | ... 16 | prometheus_httpd 17 | ... 18 | ]}, 19 | ``` 20 | 21 | ### Built-in httpd instance:
22 | ``` 23 | prometheus_httpd:start() 24 | ``` 25 | 26 | ### Telemetry metrics 27 | 28 | - `telemetry_scrape_duration_seconds` 29 | - `telemetry_scrape_size_bytes` 30 | - `telemetry_scrape_encoded_size_bytes` 31 | 32 | ### Configuration 33 | 34 | Can be configured via `prometheus_http` key of `prometheus` app env. 35 | 36 | Default configuration: 37 | ``` 38 | {prometheus, [ 39 | ... 40 | {prometheus_http, [{path, \"/metrics\"}, 41 | {format, auto}, 42 | {port, 8081}]}, 43 | ... 44 | ]} 45 | ``` 46 | """). 47 | 48 | -export([start/0]). 49 | 50 | %% httpd mod callbacks 51 | -export([do/1]). 52 | 53 | -include_lib("inets/include/httpd.hrl"). 54 | 55 | -define(SERVER_NAME, "Prometheus.io metrics."). 56 | 57 | -behaviour(application). 58 | -export([start/2, stop/1]). 59 | -behaviour(supervisor). 60 | -export([init/1]). 61 | 62 | %% =================================================================== 63 | %% Public API 64 | %% =================================================================== 65 | 66 | ?DOC(""" 67 | Starts inets httpd server with `promtheus_httpd` module enabled. 68 | 69 | Also calls `prometheus_http_impl:setup/0`. 70 | """). 71 | -spec start() -> {ok, pid()} | {error, term()}. 72 | start() -> 73 | inets:start(httpd, [ 74 | {modules, [ 75 | prometheus_httpd 76 | ]}, 77 | {port, prometheus_http_config:port()}, 78 | {server_name, ?SERVER_NAME}, 79 | {document_root, code:priv_dir(prometheus_httpd)}, 80 | {server_root, code:priv_dir(prometheus_httpd)} 81 | ]). 82 | 83 | ?DOC(false). 84 | -spec do(term()) -> {break, [term()]} | {proceed, term()}. 85 | do(Info) -> 86 | URI = Info#mod.request_uri, 87 | Headers = Info#mod.parsed_header, 88 | GetHeader = fun(Name, Default) -> 89 | proplists:get_value(Name, Headers, Default) 90 | end, 91 | 92 | %% TODO: check method, response only to GET 93 | case 94 | prometheus_http_impl:reply(#{ 95 | path => URI, 96 | headers => GetHeader, 97 | registry => undefined, 98 | standalone => standalone_p(Info) 99 | }) 100 | of 101 | {Code, RespHeaders0, Body} -> 102 | ContentLength = integer_to_list(iolist_size(Body)), 103 | RespHeaders = 104 | RespHeaders0 ++ 105 | [ 106 | {code, Code}, 107 | {content_length, ContentLength} 108 | ], 109 | {break, [{response, {response, RespHeaders, [Body]}}]}; 110 | false -> 111 | {proceed, Info#mod.data} 112 | end. 113 | 114 | %% =================================================================== 115 | %% Application & supervisor callbacks 116 | %% =================================================================== 117 | 118 | ?DOC(false). 119 | -spec start(application:start_type(), term()) -> 120 | {ok, pid()} | {ok, pid(), term()} | {error, term()}. 121 | start(_, _) -> 122 | prometheus_http_impl:setup(), 123 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 124 | 125 | ?DOC(false). 126 | -spec stop(term()) -> term(). 127 | stop(_) -> ok. 128 | 129 | ?DOC(false). 130 | -spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. 131 | init([]) -> 132 | {ok, {{one_for_one, 0, 1}, []}}. 133 | 134 | %% =================================================================== 135 | %% Private Parts 136 | %% =================================================================== 137 | 138 | standalone_p(#mod{config_db = ConfigDb}) -> 139 | case httpd_util:lookup(ConfigDb, server_name) of 140 | ?SERVER_NAME -> 141 | true; 142 | _ -> 143 | false 144 | end. 145 | -------------------------------------------------------------------------------- /test/prometheus_http_ct_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrometheusHttpdCtTest do 2 | use Prometheus.Httpd.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | 8 | test "self test" do 9 | Prometheus.Httpd.Test.self_test(%{metrics_port: 8081, 10 | metrics_path: "metrics"}) 11 | Prometheus.Httpd.Test.self_test([metrics_port: 8081, 12 | metrics_path: "/metrics"]) 13 | Prometheus.Httpd.Test.self_test(8081, 'metrics') 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/prometheus_httpd_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(prometheus_httpd_SUITE). 2 | -compile(export_all). 3 | -include_lib("common_test/include/ct.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | 6 | %% =================================================================== 7 | %% MACROS 8 | %% =================================================================== 9 | 10 | -define(README, "README.md"). 11 | 12 | -define(PROMETHEUS_ACCEPT, 13 | "application/vnd.google.protobuf;" 14 | "proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7," 15 | "text/plain;version=0.0.4;q=0.3," 16 | "application/json;schema=\"prometheus/telemetry\";version=0.0.2;q=0.2," 17 | "*/*;q=0.1" 18 | ). 19 | 20 | -define(TELEMETRY_METRICS_METADATA, [ 21 | "# TYPE telemetry_scrape_duration_seconds summary", 22 | "# HELP telemetry_scrape_duration_seconds Scrape duration", 23 | "# TYPE telemetry_scrape_size_bytes summary", 24 | "# HELP telemetry_scrape_size_bytes Scrape size, not encoded", 25 | "# TYPE telemetry_scrape_encoded_size_bytes summary", 26 | "# HELP telemetry_scrape_encoded_size_bytes Scrape size, encoded" 27 | ]). 28 | 29 | -define(AUTH_TESTS, 30 | {ok, DeniedR1} = 31 | httpc:request(get, {"http://localhost:8081/metrics", []}, [], []), 32 | ?assertMatch(403, status(DeniedR1)), 33 | {ok, DeniedR2} = 34 | httpc:request( 35 | get, 36 | {"http://localhost:8081/metrics", [{"Authorization", "Basic cXdlOnF3ZQ=="}]}, 37 | [], 38 | [] 39 | ), 40 | ?assertMatch(403, status(DeniedR2)), 41 | {ok, DeniedR3} = 42 | httpc:request( 43 | get, 44 | {"http://localhost:8081/metrics", [{"Authorization", "Basic abba"}]}, 45 | [], 46 | [] 47 | ), 48 | ?assertMatch(403, status(DeniedR3)), 49 | {ok, DeniedR4} = 50 | httpc:request( 51 | get, 52 | {"http://localhost:8081/metrics", [{"Authorization", "Bearer abba"}]}, 53 | [], 54 | [] 55 | ), 56 | ?assertMatch(403, status(DeniedR4)), 57 | {ok, BasicLPR} = 58 | httpc:request( 59 | get, 60 | {"http://localhost:8081/metrics", [{"Authorization", "Basic cXdlOnF3YQ=="}]}, 61 | [], 62 | [] 63 | ), 64 | ?assertMatch(200, status(BasicLPR)) 65 | ). 66 | 67 | %% @doc All tests of this suite. 68 | all() -> 69 | [ 70 | {group, positive} 71 | ]. 72 | 73 | %% @doc Groups of tests 74 | groups() -> 75 | [ 76 | {positive, [sequential], [ 77 | prometheus_httpd_standalone, 78 | prometheus_httpd_negotiation, 79 | prometheus_httpd_negotiation_fail, 80 | 81 | prometheus_httpd_mod, 82 | 83 | prometheus_httpd_registry, 84 | prometheus_httpd_registry_conflict, 85 | 86 | prometheus_httpd_auth_basic1, 87 | prometheus_httpd_auth_basic2, 88 | prometheus_httpd_auth_basic3, 89 | 90 | prometheus_httpd_auth_provider1, 91 | prometheus_httpd_auth_provider2, 92 | 93 | prometheus_httpd_auth_invalid 94 | ]} 95 | ]. 96 | 97 | %% @doc Start the application. 98 | init_per_suite(Config) -> 99 | {ok, _} = application:ensure_all_started(prometheus_httpd), 100 | prometheus_httpd:start(), 101 | 102 | inets:start(httpd, [ 103 | {modules, [ 104 | prometheus_httpd, 105 | mod_get 106 | ]}, 107 | {port, 8082}, 108 | {server_name, "my test_server_name"}, 109 | {document_root, code:priv_dir(prometheus_httpd)}, 110 | {server_root, code:priv_dir(prometheus_httpd)}, 111 | {mime_types, [ 112 | {"html", "text/html"}, 113 | {"css", "text/css"}, 114 | {"js", "application/x-javascript"} 115 | ]} 116 | ]), 117 | [{metrics_port, 8081}, {metrics_path, "metrics"} | Config]. 118 | 119 | end_per_testcase(_, Config) -> 120 | application:set_env(prometheus, prometheus_http, []), 121 | Config. 122 | 123 | %% @doc Stop the application. 124 | end_per_suite(Config) -> 125 | ok = application:stop(inets), 126 | ok = application:stop(prometheus), 127 | Config. 128 | 129 | %% =================================================================== 130 | %% TESTS 131 | %% =================================================================== 132 | 133 | prometheus_httpd_standalone(Config) -> 134 | prometheus_httpd_ct:self_test(Config), 135 | prometheus_httpd_ct:self_test([{metrics_port, 8081}, {metrics_path, <<"metrics">>}]), 136 | prometheus_httpd_ct:self_test([{metrics_port, 8081}, {metrics_path, <<"/metrics">>}]), 137 | 138 | {ok, HTMLResponse} = httpc:request("http://localhost:8081/random_path"), 139 | ?assertMatch(200, status(HTMLResponse)), 140 | ExpectedHTMLCT = "text/html", 141 | ?assertMatch( 142 | [ 143 | {"content-length", ExpectedHTMLCL}, 144 | {"content-type", ExpectedHTMLCT} 145 | | _ 146 | ] when 147 | ExpectedHTMLCL > 0, 148 | headers(HTMLResponse) 149 | ), 150 | Path = prometheus_http_config:path(), 151 | ?assertMatch({match, _}, re:run(body(HTMLResponse), ["href=\"", Path, "\""])). 152 | 153 | prometheus_httpd_negotiation(_Config) -> 154 | {ok, TextResponse} = 155 | httpc:request( 156 | get, {"http://localhost:8081/metrics", [{"Accept-Encoding", "deflate"}]}, [], [] 157 | ), 158 | ?assertMatch(200, status(TextResponse)), 159 | TextCT = prometheus_text_format:content_type(), 160 | ExpectedTextCT = binary_to_list(TextCT), 161 | ?assertMatch( 162 | [ 163 | {"content-encoding", "deflate"}, 164 | {"content-length", ExpectedTextCL}, 165 | {"content-type", ExpectedTextCT} 166 | | _ 167 | ] when 168 | ExpectedTextCL > 0, 169 | headers(TextResponse) 170 | ), 171 | ?assert(iolist_size(body(TextResponse)) > 0), 172 | 173 | {ok, ProtobufResponse} = 174 | httpc:request( 175 | get, 176 | {"http://localhost:8081/metrics", [ 177 | {"Accept", ?PROMETHEUS_ACCEPT}, 178 | {"Accept-Encoding", "gzip, sdch"} 179 | ]}, 180 | [], 181 | [] 182 | ), 183 | ?assertMatch(200, status(ProtobufResponse)), 184 | ProtobufCT = prometheus_protobuf_format:content_type(), 185 | ExpectedProtobufCT = binary_to_list(ProtobufCT), 186 | ?assertMatch( 187 | [ 188 | {"content-encoding", "gzip"}, 189 | {"content-length", ExpectedProtobufCL}, 190 | {"content-type", ExpectedProtobufCT} 191 | | _ 192 | ] when 193 | ExpectedProtobufCL > 0, 194 | headers(ProtobufResponse) 195 | ), 196 | ?assert(iolist_size(zlib:gunzip(body(ProtobufResponse))) > 0), 197 | 198 | application:set_env( 199 | prometheus, 200 | prometheus_http, 201 | [{format, prometheus_protobuf_format}] 202 | ), 203 | {ok, ProtobufResponse1} = 204 | httpc:request(get, {"http://localhost:8081/metrics", []}, [], []), 205 | ?assertMatch(200, status(ProtobufResponse1)), 206 | ProtobufCT = prometheus_protobuf_format:content_type(), 207 | ExpectedProtobufCT = binary_to_list(ProtobufCT), 208 | ?assertMatch( 209 | [ 210 | {"content-encoding", "identity"}, 211 | {"content-length", ExpectedProtobufCL1}, 212 | {"content-type", ExpectedProtobufCT} 213 | | _ 214 | ] when 215 | ExpectedProtobufCL1 > 0, 216 | headers(ProtobufResponse1) 217 | ), 218 | ?assert(iolist_size(body(ProtobufResponse)) > 0). 219 | 220 | prometheus_httpd_negotiation_fail(_Config) -> 221 | {ok, IdentityResponse} = 222 | httpc:request(get, {"http://localhost:8081/metrics", [{"Accept-Encoding", "qwe"}]}, [], []), 223 | ?assertMatch(200, status(IdentityResponse)), 224 | IdentityCT = prometheus_text_format:content_type(), 225 | ExpectedIdentityCT = binary_to_list(IdentityCT), 226 | ?assertMatch( 227 | [ 228 | {"content-encoding", "identity"}, 229 | {"content-length", ExpectedIdentityCL}, 230 | {"content-type", ExpectedIdentityCT} 231 | | _ 232 | ] when 233 | ExpectedIdentityCL > 0, 234 | headers(IdentityResponse) 235 | ), 236 | 237 | {ok, FEResponse} = 238 | httpc:request( 239 | get, {"http://localhost:8081/metrics", [{"Accept-Encoding", "qwe, *;q=0"}]}, [], [] 240 | ), 241 | ?assertMatch(406, status(FEResponse)), 242 | ?assertMatch( 243 | [ 244 | {"content-length", "0"}, 245 | {"content-type", "text/html"} 246 | | _ 247 | ], 248 | headers(FEResponse) 249 | ), 250 | 251 | {ok, CTResponse} = 252 | httpc:request(get, {"http://localhost:8081/metrics", [{"Accept", "image/png"}]}, [], []), 253 | ?assertMatch(406, status(CTResponse)), 254 | ?assertMatch( 255 | [ 256 | {"content-length", "0"}, 257 | {"content-type", "text/html"} 258 | | _ 259 | ], 260 | headers(CTResponse) 261 | ). 262 | 263 | prometheus_httpd_mod(_Config) -> 264 | {ok, MetricsResponse} = httpc:request("http://localhost:8082/metrics"), 265 | ?assertMatch(200, status(MetricsResponse)), 266 | MetricsCT = prometheus_text_format:content_type(), 267 | ExpecteMetricsCT = binary_to_list(MetricsCT), 268 | ?assertMatch( 269 | [ 270 | {"content-encoding", "identity"}, 271 | {"content-length", ExpectedMetricsCL}, 272 | {"content-type", ExpecteMetricsCT} 273 | | _ 274 | ] when 275 | ExpectedMetricsCL > 0, 276 | headers(MetricsResponse) 277 | ), 278 | MetricsBody = body(MetricsResponse), 279 | ?assertMatch(true, all_telemetry_metrics_present(MetricsBody)), 280 | 281 | {ok, HTMLResponse} = httpc:request("http://localhost:8082/index.html"), 282 | ?assertMatch(200, status(HTMLResponse)), 283 | ExpectedHTMLCT = "text/html", 284 | ?assertMatch( 285 | [ 286 | {"content-length", ExpectedHTMLCL}, 287 | {"content-type", ExpectedHTMLCT} 288 | | _ 289 | ] when 290 | ExpectedHTMLCL > 0, 291 | headers(HTMLResponse) 292 | ), 293 | ?assertMatch({match, _}, re:run(body(HTMLResponse), "M_E_T_R_I_C_S")), 294 | 295 | {ok, CTResponse} = 296 | httpc:request(get, {"http://localhost:8082/qwe", []}, [], []), 297 | ?assertMatch(404, status(CTResponse)), 298 | ?assertMatch( 299 | [ 300 | {"content-length", CL404}, 301 | {"content-type", "text/html"} 302 | | _ 303 | ] when 304 | CL404 > 0, 305 | headers(CTResponse) 306 | ). 307 | 308 | prometheus_httpd_registry(_Config) -> 309 | prometheus_counter:new([{registry, qwe}, {name, qwe}, {help, ""}]), 310 | prometheus_counter:inc(qwe, qwe, [], 10), 311 | 312 | {ok, MetricsResponse} = httpc:request("http://localhost:8081/metrics/qwe"), 313 | ?assertMatch(200, status(MetricsResponse)), 314 | MetricsCT = prometheus_text_format:content_type(), 315 | ExpecteMetricsCT = binary_to_list(MetricsCT), 316 | ?assertMatch( 317 | [ 318 | {"content-encoding", "identity"}, 319 | {"content-length", ExpectedMetricsCL}, 320 | {"content-type", ExpecteMetricsCT} 321 | | _ 322 | ] when 323 | ExpectedMetricsCL > 0, 324 | headers(MetricsResponse) 325 | ), 326 | MetricsBody = body(MetricsResponse), 327 | ?assertMatch(false, all_telemetry_metrics_present(MetricsBody)), 328 | ?assertMatch({match, _}, re:run(MetricsBody, "# TYPE qwe counter")), 329 | 330 | {ok, IRResponse} = 331 | httpc:request(get, {"http://localhost:8082/metrics/qwa", []}, [], []), 332 | ?assertMatch(404, status(IRResponse)), 333 | ?assertMatch( 334 | [ 335 | {"content-length", CL404}, 336 | {"content-type", "text/html"} 337 | | _ 338 | ] when 339 | CL404 > 0, 340 | headers(IRResponse) 341 | ). 342 | 343 | prometheus_httpd_registry_conflict(_Config) -> 344 | application:set_env( 345 | prometheus, 346 | prometheus_http, 347 | [{registry, default}] 348 | ), 349 | 350 | {ok, DeniedR1} = 351 | httpc:request(get, {"http://localhost:8081/metrics/qwe", []}, [], []), 352 | ?assertMatch(409, status(DeniedR1)). 353 | 354 | prometheus_httpd_auth_basic1(_Config) -> 355 | application:set_env(prometheus, prometheus_http, [{authorization, {basic, "qwe", "qwa"}}]), 356 | 357 | ?AUTH_TESTS. 358 | 359 | prometheus_httpd_auth_basic2(_Config) -> 360 | application:set_env(prometheus, prometheus_http, [{authorization, {basic, ?MODULE}}]), 361 | 362 | ?AUTH_TESTS. 363 | 364 | prometheus_httpd_auth_basic3(_Config) -> 365 | application:set_env( 366 | prometheus, 367 | prometheus_http, 368 | [{authorization, {basic, {?MODULE, authorize}}}] 369 | ), 370 | 371 | ?AUTH_TESTS. 372 | 373 | prometheus_httpd_auth_provider1(_Config) -> 374 | application:set_env( 375 | prometheus, 376 | prometheus_http, 377 | [{authorization, {?MODULE, authorize}}] 378 | ), 379 | 380 | ?AUTH_TESTS. 381 | 382 | prometheus_httpd_auth_provider2(_Config) -> 383 | application:set_env( 384 | prometheus, 385 | prometheus_http, 386 | [{authorization, ?MODULE}] 387 | ), 388 | 389 | ?AUTH_TESTS. 390 | 391 | prometheus_httpd_auth_invalid(_Config) -> 392 | application:set_env( 393 | prometheus, 394 | prometheus_http, 395 | [{authorization, "qwe"}] 396 | ), 397 | 398 | {ok, DeniedR1} = 399 | httpc:request(get, {"http://localhost:8081/metrics", []}, [], []), 400 | ?assertMatch(500, status(DeniedR1)). 401 | 402 | authorize("qwe", "qwa") -> 403 | true; 404 | authorize(_, _) -> 405 | false. 406 | 407 | authorize(#{headers := Headers}) -> 408 | case Headers("authorization", undefined) of 409 | undefined -> 410 | false; 411 | "Basic cXdlOnF3ZQ==" -> 412 | false; 413 | "Basic abba" -> 414 | false; 415 | "Bearer abba" -> 416 | false; 417 | _ -> 418 | true 419 | end. 420 | 421 | %% =================================================================== 422 | %% Private parts 423 | %% =================================================================== 424 | 425 | all_telemetry_metrics_present(Body) -> 426 | lists:all( 427 | fun(Metric) -> 428 | case re:run(Body, Metric) of 429 | {match, _} -> true; 430 | _ -> false 431 | end 432 | end, 433 | ?TELEMETRY_METRICS_METADATA 434 | ). 435 | 436 | %%% Helpers 437 | 438 | status({{_, Status, _}, _, _}) -> 439 | Status. 440 | body({_, _, Body}) -> 441 | Body. 442 | 443 | headers({_, Headers, _}) -> 444 | lists:sort(Headers). 445 | -------------------------------------------------------------------------------- /test/prometheus_httpd_ct.erl: -------------------------------------------------------------------------------- 1 | -module(prometheus_httpd_ct). 2 | 3 | -export([self_test/1]). 4 | -export([self_test/2]). 5 | 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(TELEMETRY_METRICS_METADATA, [ 9 | "# TYPE telemetry_scrape_duration_seconds summary", 10 | "# HELP telemetry_scrape_duration_seconds Scrape duration", 11 | "# TYPE telemetry_scrape_size_bytes summary", 12 | "# HELP telemetry_scrape_size_bytes Scrape size, not encoded", 13 | "# TYPE telemetry_scrape_encoded_size_bytes summary", 14 | "# HELP telemetry_scrape_encoded_size_bytes Scrape size, encoded" 15 | ]). 16 | 17 | %% =================================================================== 18 | %% API 19 | %% =================================================================== 20 | 21 | -spec self_test(proplists:proplist()) -> any(). 22 | self_test(Config) -> 23 | Path = proplists:get_value(metrics_path, Config), 24 | Port = proplists:get_value(metrics_port, Config), 25 | self_test(Port, Path). 26 | 27 | -spec self_test(integer(), string() | binary()) -> any(). 28 | self_test(Port, Path0) -> 29 | Path = normalize_path(Path0), 30 | URL = format_to_string("http://localhost:~p/~s", [Port, Path]), 31 | {ok, MetricsResponse} = httpc:request(URL), 32 | ?assertMatch(200, status(MetricsResponse)), 33 | MetricsCT = prometheus_text_format:content_type(), 34 | ExpectedMetricsCT = binary_to_list(MetricsCT), 35 | ?assertMatch( 36 | [ 37 | {"content-encoding", "identity"}, 38 | {"content-length", ExpectedMetricsCL}, 39 | {"content-type", ExpectedMetricsCT} 40 | | _ 41 | ] when 42 | ExpectedMetricsCL > 0, 43 | headers(MetricsResponse) 44 | ), 45 | MetricsBody = body(MetricsResponse), 46 | ?assertMatch(true, all_telemetry_metrics_present(MetricsBody)). 47 | 48 | %% =================================================================== 49 | %% Private functions 50 | %% =================================================================== 51 | 52 | all_telemetry_metrics_present(Body) -> 53 | lists:all( 54 | fun(Metric) -> 55 | case re:run(Body, Metric) of 56 | {match, _} -> true; 57 | _ -> false 58 | end 59 | end, 60 | ?TELEMETRY_METRICS_METADATA 61 | ). 62 | 63 | status({{_, Status, _}, _, _}) -> 64 | Status. 65 | body({_, _, Body}) -> 66 | Body. 67 | 68 | headers({_, Headers, _}) -> 69 | lists:sort(Headers). 70 | 71 | normalize_path([$/ | Rest]) -> 72 | Rest; 73 | normalize_path(<<"/", Rest/binary>>) -> 74 | Rest; 75 | normalize_path(Path) -> 76 | Path. 77 | 78 | format_to_string(Format, Args) -> 79 | binary_to_list( 80 | iolist_to_binary( 81 | io_lib:format(Format, Args) 82 | ) 83 | ). 84 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule Prometheus.Httpd.Case do 4 | defmacro __using__(_opts) do 5 | quote do 6 | 7 | use ExUnit.Case 8 | import ExUnit.CaptureIO 9 | 10 | setup do 11 | {:ok, _} = :application.ensure_all_started(:prometheus_httpd) 12 | :prometheus_httpd.start() 13 | 14 | on_exit fn -> 15 | :application.set_env(:prometheus, :prometheus_http, []) 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | --------------------------------------------------------------------------------