├── .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 | [](https://hex.pm/packages/prometheus_httpd)
4 | [](https://hex.pm/packages/prometheus_httpd)
5 | [](https://hexdocs.pm/prometheus_httpd/)
6 | [](https://github.com/prometheus-erl/prometheus-httpd/actions/workflows/ci.yml)
7 | [](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 | 
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 |
7 |
8 |
20 |
21 |
28 |
29 |
Integrations
30 |
41 |
42 |
Dashboards
43 |
46 |
47 |
Torch Icon Copyright (c) IconBeast.com
48 |
Copyright (c) 2017 Ilya Khaprov
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 |
--------------------------------------------------------------------------------