Skip to content

Commit c9f16ad

Browse files
committed
feat: native metrics with Prometheus support (#772, #560)
Replace external metrics library with hackney-native metrics abstraction: - Add hackney_metrics_backend behaviour for pluggable backends - Add hackney_metrics_dummy backend (no-op, default) - Add hackney_metrics_prometheus backend (opt-in) - Rewrite hackney_metrics as API facade routing to backends - Update hackney_manager to use new metrics API (request metrics) - Update hackney_pool to use new metrics API (pool metrics) - Remove external metrics library dependency Metric types fixed (issue #560): - Pool free_count/in_use_count now use gauges (not histograms) - Request active count now uses gauge (not counter) New metric names (Prometheus-compatible): - hackney_requests_total (counter, labels: host) - hackney_requests_active (gauge, labels: host) - hackney_requests_finished_total (counter, labels: host) - hackney_request_duration_seconds (histogram, labels: host) - hackney_pool_free_count (gauge, labels: pool) - hackney_pool_in_use_count (gauge, labels: pool) - hackney_pool_checkouts_total (counter, labels: pool) Configuration: - Default: hackney_metrics_dummy (no-op, zero overhead) - To enable Prometheus: {hackney, [{metrics_backend, prometheus}]} Closes #772, closes #560
1 parent 1aa5945 commit c9f16ad

10 files changed

+404
-95
lines changed

rebar.config

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
{idna, "~>6.1.0"},
3838
{mimerl, "~>1.4"},
3939
{certifi, "~>2.15.0"},
40-
{metrics, "~>1.0.0"},
4140
{parse_trans, "3.4.1"},
4241
{ssl_verify_fun, "~>1.1.0"},
4342
{unicode_util_compat, "~>0.7.1"}
@@ -85,6 +84,8 @@
8584
error_handling%,
8685
%unknown
8786
]},
87+
%% Exclude prometheus backend - prometheus is an optional dependency
88+
{exclude_mods, [hackney_metrics_prometheus]},
8889
{base_plt_apps, [erts, stdlib, kernel, crypto, runtime_tools]},
8990
{plt_apps, top_level_deps},
9091
{plt_extra_apps, []},

src/hackney.app.src

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
mimerl,
1717
certifi,
1818
ssl_verify_fun,
19-
metrics,
2019
unicode_util_compat]},
2120
{included_applications, []},
2221
{mod, { hackney_app, []}},

src/hackney_manager.erl

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212

1313
%% Metrics API
1414
-export([start_request/1,
15-
finish_request/2,
16-
get_metrics_engine/0]).
15+
finish_request/2]).
1716

1817
%% Backward compatibility API
1918
-export([get_state/1, async_response_pid/1]).
@@ -23,9 +22,7 @@
2322
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
2423
terminate/2, code_change/3]).
2524

26-
-record(state, {
27-
metrics_engine
28-
}).
25+
-record(state, {}).
2926

3027
%%====================================================================
3128
%% API
@@ -41,11 +38,6 @@ start_request(Host) ->
4138
finish_request(Host, StartTime) ->
4239
gen_server:cast(?MODULE, {finish_request, Host, StartTime}).
4340

44-
%% @doc Get the current metrics engine.
45-
-spec get_metrics_engine() -> module().
46-
get_metrics_engine() ->
47-
hackney_metrics:get_engine().
48-
4941
%% @doc Check the state of a connection (backward compatibility).
5042
%% In the old architecture, this tracked request state.
5143
%% In the new architecture, we simply check if the connection process is alive.
@@ -63,20 +55,15 @@ get_state(ConnPid) when is_pid(ConnPid) ->
6355
get_state(_) ->
6456
req_not_found.
6557

66-
%% @doc Check if a connection is in async mode (backward compatibility).
67-
%% In the old architecture, this returned the async response process PID.
68-
%% In the new architecture, we check if the connection process is in async mode.
69-
%% Returns `{error, req_not_async}' if not in async mode.
70-
-spec async_response_pid(pid() | term()) -> {ok, pid()} | {error, req_not_async}.
71-
async_response_pid(ConnPid) when is_pid(ConnPid) ->
72-
case is_process_alive(ConnPid) of
73-
false -> {error, req_not_async};
74-
true ->
75-
case hackney_conn:get_state(ConnPid) of
76-
{ok, State} when State =:= receiving; State =:= streaming ->
77-
{ok, ConnPid};
78-
_ ->
79-
{error, req_not_async}
58+
%% @doc Get the async response pid (backward compatibility).
59+
-spec async_response_pid(pid()) -> {ok, pid()} | {error, req_not_found | req_not_async}.
60+
async_response_pid(Ref) when is_pid(Ref) ->
61+
case get_state(Ref) of
62+
req_not_found -> {error, req_not_found};
63+
State ->
64+
case proplists:get_value(async, State, false) of
65+
false -> {error, req_not_async};
66+
true -> {ok, Ref}
8067
end
8168
end;
8269
async_response_pid(_) ->
@@ -90,28 +77,27 @@ start_link() ->
9077
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
9178

9279
init([]) ->
93-
%% Initialize metrics
94-
Engine = hackney_metrics:get_engine(),
95-
_ = metrics:new(Engine, counter, [hackney, nb_requests]),
96-
_ = metrics:new(Engine, counter, [hackney, total_requests]),
97-
_ = metrics:new(Engine, counter, [hackney, finished_requests]),
98-
{ok, #state{metrics_engine = Engine}}.
80+
{ok, #state{}}.
9981

10082
handle_call(_Request, _From, State) ->
10183
{reply, ok, State}.
10284

103-
handle_cast({start_request, Host}, #state{metrics_engine = Engine} = State) ->
104-
_ = metrics:increment_counter(Engine, [hackney, Host, nb_requests]),
105-
_ = metrics:increment_counter(Engine, [hackney, nb_requests]),
106-
_ = metrics:increment_counter(Engine, [hackney, total_requests]),
85+
handle_cast({start_request, Host}, State) ->
86+
HostBin = to_binary(Host),
87+
Labels = #{host => HostBin},
88+
_ = hackney_metrics:counter_inc(hackney_requests_total, Labels),
89+
_ = hackney_metrics:gauge_inc(hackney_requests_active, Labels),
10790
{noreply, State};
10891

109-
handle_cast({finish_request, Host, StartTime}, #state{metrics_engine = Engine} = State) ->
110-
RequestTime = timer:now_diff(os:timestamp(), StartTime) / 1000,
111-
_ = metrics:update_histogram(Engine, [hackney, Host, request_time], RequestTime),
112-
_ = metrics:decrement_counter(Engine, [hackney, Host, nb_requests]),
113-
_ = metrics:decrement_counter(Engine, [hackney, nb_requests]),
114-
_ = metrics:increment_counter(Engine, [hackney, finished_requests]),
92+
handle_cast({finish_request, Host, StartTime}, State) ->
93+
HostBin = to_binary(Host),
94+
Labels = #{host => HostBin},
95+
%% Calculate duration in seconds (Prometheus convention)
96+
DurationMicros = timer:now_diff(os:timestamp(), StartTime),
97+
DurationSeconds = DurationMicros / 1000000,
98+
_ = hackney_metrics:histogram_observe(hackney_request_duration_seconds, Labels, DurationSeconds),
99+
_ = hackney_metrics:gauge_dec(hackney_requests_active, Labels),
100+
_ = hackney_metrics:counter_inc(hackney_requests_finished_total, Labels),
115101
{noreply, State};
116102

117103
handle_cast(_Msg, State) ->
@@ -125,3 +111,11 @@ terminate(_Reason, _State) ->
125111

126112
code_change(_OldVsn, State, _Extra) ->
127113
{ok, State}.
114+
115+
%%====================================================================
116+
%% Internal functions
117+
%%====================================================================
118+
119+
to_binary(Host) when is_binary(Host) -> Host;
120+
to_binary(Host) when is_list(Host) -> list_to_binary(Host);
121+
to_binary(Host) when is_atom(Host) -> atom_to_binary(Host, utf8).

src/hackney_metrics.erl

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
%%% This file is part of hackney released under the Apache 2 license.
44
%%% See the NOTICE for more information.
55
%%%
6-
%%% Copyright (c) 2012-2018 Benoît Chesneau <benoitc@e-engura.org>
6+
%%% Copyright (c) 2012-2026 Benoît Chesneau <benoitc@e-engura.org>
77
%%%
88

99
-module(hackney_metrics).
@@ -12,16 +12,122 @@
1212
%% API
1313
-export([
1414
init/0,
15-
get_engine/0
15+
get_backend/0
1616
]).
1717

18+
%% Counter operations
19+
-export([
20+
counter_inc/2,
21+
counter_inc/3
22+
]).
23+
24+
%% Gauge operations
25+
-export([
26+
gauge_set/3,
27+
gauge_inc/2,
28+
gauge_dec/2
29+
]).
30+
31+
%% Histogram operations
32+
-export([
33+
histogram_observe/3
34+
]).
35+
36+
%% Metric declarations
37+
-export([
38+
declare_counter/3,
39+
declare_gauge/3,
40+
declare_histogram/3,
41+
declare_histogram/4,
42+
declare_pool_metrics/1
43+
]).
1844

1945
-include("hackney.hrl").
2046

47+
%% Default duration histogram buckets (in seconds)
48+
-define(DEFAULT_DURATION_BUCKETS, [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]).
2149

50+
%% @doc Initialize the metrics system.
51+
%% Determines the backend to use and declares all hackney metrics.
2252
init() ->
23-
Metrics = metrics:init(hackney_util:mod_metrics()),
24-
ets:insert(?CONFIG, {mod_metrics, Metrics}).
53+
Backend = hackney_util:mod_metrics(),
54+
ets:insert(?CONFIG, {metrics_backend, Backend}),
55+
declare_metrics(Backend).
56+
57+
%% @doc Get the current metrics backend module.
58+
get_backend() ->
59+
try
60+
ets:lookup_element(?CONFIG, metrics_backend, 2)
61+
catch
62+
error:badarg ->
63+
%% ETS table not ready yet, return dummy backend
64+
hackney_metrics_dummy
65+
end.
66+
67+
%% @doc Increment a counter by 1.
68+
counter_inc(Name, Labels) ->
69+
(get_backend()):counter_inc(Name, Labels).
70+
71+
%% @doc Increment a counter by a value.
72+
counter_inc(Name, Labels, Value) ->
73+
(get_backend()):counter_inc(Name, Labels, Value).
74+
75+
%% @doc Set a gauge to a value.
76+
gauge_set(Name, Labels, Value) ->
77+
(get_backend()):gauge_set(Name, Labels, Value).
78+
79+
%% @doc Increment a gauge by 1.
80+
gauge_inc(Name, Labels) ->
81+
(get_backend()):gauge_inc(Name, Labels).
82+
83+
%% @doc Decrement a gauge by 1.
84+
gauge_dec(Name, Labels) ->
85+
(get_backend()):gauge_dec(Name, Labels).
86+
87+
%% @doc Observe a value for a histogram.
88+
histogram_observe(Name, Labels, Value) ->
89+
(get_backend()):histogram_observe(Name, Labels, Value).
90+
91+
%% @doc Declare a counter metric.
92+
declare_counter(Name, Help, LabelKeys) ->
93+
(get_backend()):declare_counter(Name, Help, LabelKeys).
94+
95+
%% @doc Declare a gauge metric.
96+
declare_gauge(Name, Help, LabelKeys) ->
97+
(get_backend()):declare_gauge(Name, Help, LabelKeys).
98+
99+
%% @doc Declare a histogram metric with default buckets.
100+
declare_histogram(Name, Help, LabelKeys) ->
101+
(get_backend()):declare_histogram(Name, Help, LabelKeys).
102+
103+
%% @doc Declare a histogram metric with custom buckets.
104+
declare_histogram(Name, Help, LabelKeys, Buckets) ->
105+
(get_backend()):declare_histogram(Name, Help, LabelKeys, Buckets).
106+
107+
%% @doc Declare pool-specific metrics.
108+
%% Called when a new pool is created.
109+
declare_pool_metrics(_PoolName) ->
110+
Backend = get_backend(),
111+
%% Only declare once (idempotent for prometheus)
112+
Backend:declare_gauge(hackney_pool_free_count,
113+
<<"Number of free/available connections in the pool">>, [pool]),
114+
Backend:declare_gauge(hackney_pool_in_use_count,
115+
<<"Number of connections currently in use">>, [pool]),
116+
Backend:declare_counter(hackney_pool_checkouts_total,
117+
<<"Total number of connection checkouts">>, [pool]),
118+
ok.
25119

26-
get_engine() ->
27-
ets:lookup_element(?CONFIG, mod_metrics, 2).
120+
%% @private
121+
%% Declare all hackney metrics at startup.
122+
declare_metrics(Backend) ->
123+
%% Request metrics
124+
Backend:declare_counter(hackney_requests_total,
125+
<<"Total number of HTTP requests started">>, [host]),
126+
Backend:declare_gauge(hackney_requests_active,
127+
<<"Number of currently active HTTP requests">>, [host]),
128+
Backend:declare_counter(hackney_requests_finished_total,
129+
<<"Total number of HTTP requests finished">>, [host]),
130+
Backend:declare_histogram(hackney_request_duration_seconds,
131+
<<"HTTP request duration in seconds">>, [host], ?DEFAULT_DURATION_BUCKETS),
132+
%% Pool metrics are declared when pools are created
133+
ok.

src/hackney_metrics_backend.erl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
%%% -*- erlang -*-
2+
%%%
3+
%%% This file is part of hackney released under the Apache 2 license.
4+
%%% See the NOTICE for more information.
5+
%%%
6+
%%% Copyright (c) 2012-2026 Benoît Chesneau <benoitc@e-engura.org>
7+
%%%
8+
9+
-module(hackney_metrics_backend).
10+
-author("benoitc").
11+
12+
%% Behaviour callbacks for hackney metrics backends
13+
%%
14+
%% Implementations must export all callback functions.
15+
%% See hackney_metrics_dummy for a reference implementation.
16+
17+
%% Counter operations (monotonically increasing)
18+
-callback counter_inc(Name :: atom(), Labels :: map()) -> ok.
19+
-callback counter_inc(Name :: atom(), Labels :: map(), Value :: number()) -> ok.
20+
21+
%% Gauge operations (can go up or down)
22+
-callback gauge_set(Name :: atom(), Labels :: map(), Value :: number()) -> ok.
23+
-callback gauge_inc(Name :: atom(), Labels :: map()) -> ok.
24+
-callback gauge_dec(Name :: atom(), Labels :: map()) -> ok.
25+
26+
%% Histogram operations (for timing/distribution measurements)
27+
-callback histogram_observe(Name :: atom(), Labels :: map(), Value :: number()) -> ok.
28+
29+
%% Metric lifecycle
30+
-callback declare_counter(Name :: atom(), Help :: binary(), LabelKeys :: [atom()]) -> ok.
31+
-callback declare_gauge(Name :: atom(), Help :: binary(), LabelKeys :: [atom()]) -> ok.
32+
-callback declare_histogram(Name :: atom(), Help :: binary(), LabelKeys :: [atom()]) -> ok.
33+
-callback declare_histogram(Name :: atom(), Help :: binary(), LabelKeys :: [atom()], Buckets :: [number()]) -> ok.

src/hackney_metrics_dummy.erl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
%%% -*- erlang -*-
2+
%%%
3+
%%% This file is part of hackney released under the Apache 2 license.
4+
%%% See the NOTICE for more information.
5+
%%%
6+
%%% Copyright (c) 2012-2026 Benoît Chesneau <benoitc@e-engura.org>
7+
%%%
8+
9+
-module(hackney_metrics_dummy).
10+
-author("benoitc").
11+
12+
-behaviour(hackney_metrics_backend).
13+
14+
%% hackney_metrics_backend callbacks
15+
-export([
16+
counter_inc/2,
17+
counter_inc/3,
18+
gauge_set/3,
19+
gauge_inc/2,
20+
gauge_dec/2,
21+
histogram_observe/3,
22+
declare_counter/3,
23+
declare_gauge/3,
24+
declare_histogram/3,
25+
declare_histogram/4
26+
]).
27+
28+
%% Counter operations - no-op
29+
counter_inc(_Name, _Labels) -> ok.
30+
counter_inc(_Name, _Labels, _Value) -> ok.
31+
32+
%% Gauge operations - no-op
33+
gauge_set(_Name, _Labels, _Value) -> ok.
34+
gauge_inc(_Name, _Labels) -> ok.
35+
gauge_dec(_Name, _Labels) -> ok.
36+
37+
%% Histogram operations - no-op
38+
histogram_observe(_Name, _Labels, _Value) -> ok.
39+
40+
%% Metric lifecycle - no-op
41+
declare_counter(_Name, _Help, _LabelKeys) -> ok.
42+
declare_gauge(_Name, _Help, _LabelKeys) -> ok.
43+
declare_histogram(_Name, _Help, _LabelKeys) -> ok.
44+
declare_histogram(_Name, _Help, _LabelKeys, _Buckets) -> ok.

0 commit comments

Comments
 (0)