Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ rebar3.crashdump
doc/
priv/*.so
priv/*.dll
rebar.lock
14 changes: 13 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
]}.

{xref_checks, [undefined_function_calls]}.
%% Ignore xref warnings for optional prometheus dependency
{xref_ignores, [
{prometheus_counter, inc, 3},
{prometheus_counter, declare, 1},
{prometheus_gauge, set, 3},
{prometheus_gauge, inc, 2},
{prometheus_gauge, dec, 2},
{prometheus_gauge, declare, 1},
{prometheus_histogram, observe, 3},
{prometheus_histogram, declare, 1}
]}.

{cover_enabled, true}.
{eunit_opts, [verbose]}.
Expand All @@ -37,7 +48,6 @@
{idna, "~>6.1.0"},
{mimerl, "~>1.4"},
{certifi, "~>2.15.0"},
{metrics, "~>1.0.0"},
{parse_trans, "3.4.1"},
{ssl_verify_fun, "~>1.1.0"},
{unicode_util_compat, "~>0.7.1"}
Expand Down Expand Up @@ -85,6 +95,8 @@
error_handling%,
%unknown
]},
%% Exclude prometheus backend - prometheus is an optional dependency
{exclude_mods, [hackney_metrics_prometheus]},
{base_plt_apps, [erts, stdlib, kernel, crypto, runtime_tools]},
{plt_apps, top_level_deps},
{plt_extra_apps, []},
Expand Down
26 changes: 0 additions & 26 deletions rebar.lock

This file was deleted.

1 change: 0 additions & 1 deletion src/hackney.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
mimerl,
certifi,
ssl_verify_fun,
metrics,
unicode_util_compat]},
{included_applications, []},
{mod, { hackney_app, []}},
Expand Down
79 changes: 36 additions & 43 deletions src/hackney_manager.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@

%% Metrics API
-export([start_request/1,
finish_request/2,
get_metrics_engine/0]).
finish_request/2]).

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

-record(state, {
metrics_engine
}).
-record(state, {}).

%%====================================================================
%% API
Expand All @@ -41,16 +38,11 @@ start_request(Host) ->
finish_request(Host, StartTime) ->
gen_server:cast(?MODULE, {finish_request, Host, StartTime}).

%% @doc Get the current metrics engine.
-spec get_metrics_engine() -> module().
get_metrics_engine() ->
hackney_metrics:get_engine().

%% @doc Check the state of a connection (backward compatibility).
%% In the old architecture, this tracked request state.
%% In the new architecture, we simply check if the connection process is alive.
%% Returns `req_not_found' if the process is dead, or the connection state.
-spec get_state(pid() | term()) -> req_not_found | term().
%% Returns `req_not_found' if the process is dead, or the connection state name.
-spec get_state(pid() | term()) -> req_not_found | atom().
get_state(ConnPid) when is_pid(ConnPid) ->
case is_process_alive(ConnPid) of
false -> req_not_found;
Expand All @@ -63,21 +55,15 @@ get_state(ConnPid) when is_pid(ConnPid) ->
get_state(_) ->
req_not_found.

%% @doc Check if a connection is in async mode (backward compatibility).
%% In the old architecture, this returned the async response process PID.
%% In the new architecture, we check if the connection process is in async mode.
%% Returns `{error, req_not_async}' if not in async mode.
-spec async_response_pid(pid() | term()) -> {ok, pid()} | {error, req_not_async}.
async_response_pid(ConnPid) when is_pid(ConnPid) ->
case is_process_alive(ConnPid) of
false -> {error, req_not_async};
true ->
case hackney_conn:get_state(ConnPid) of
{ok, State} when State =:= receiving; State =:= streaming ->
{ok, ConnPid};
_ ->
{error, req_not_async}
end
%% @doc Get the async response pid (backward compatibility).
%% In the new architecture, all streaming connections are considered "async".
-spec async_response_pid(pid()) -> {ok, pid()} | {error, req_not_found | req_not_async}.
async_response_pid(Ref) when is_pid(Ref) ->
case get_state(Ref) of
req_not_found -> {error, req_not_found};
streaming -> {ok, Ref};
streaming_once -> {ok, Ref};
_ -> {error, req_not_async}
end;
async_response_pid(_) ->
{error, req_not_async}.
Expand All @@ -90,28 +76,27 @@ start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
%% Initialize metrics
Engine = hackney_metrics:get_engine(),
_ = metrics:new(Engine, counter, [hackney, nb_requests]),
_ = metrics:new(Engine, counter, [hackney, total_requests]),
_ = metrics:new(Engine, counter, [hackney, finished_requests]),
{ok, #state{metrics_engine = Engine}}.
{ok, #state{}}.

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

handle_cast({start_request, Host}, #state{metrics_engine = Engine} = State) ->
_ = metrics:increment_counter(Engine, [hackney, Host, nb_requests]),
_ = metrics:increment_counter(Engine, [hackney, nb_requests]),
_ = metrics:increment_counter(Engine, [hackney, total_requests]),
handle_cast({start_request, Host}, State) ->
HostBin = to_binary(Host),
Labels = #{host => HostBin},
_ = hackney_metrics:counter_inc(hackney_requests_total, Labels),
_ = hackney_metrics:gauge_inc(hackney_requests_active, Labels),
{noreply, State};

handle_cast({finish_request, Host, StartTime}, #state{metrics_engine = Engine} = State) ->
RequestTime = timer:now_diff(os:timestamp(), StartTime) / 1000,
_ = metrics:update_histogram(Engine, [hackney, Host, request_time], RequestTime),
_ = metrics:decrement_counter(Engine, [hackney, Host, nb_requests]),
_ = metrics:decrement_counter(Engine, [hackney, nb_requests]),
_ = metrics:increment_counter(Engine, [hackney, finished_requests]),
handle_cast({finish_request, Host, StartTime}, State) ->
HostBin = to_binary(Host),
Labels = #{host => HostBin},
%% Calculate duration in seconds (Prometheus convention)
DurationMicros = timer:now_diff(os:timestamp(), StartTime),
DurationSeconds = DurationMicros / 1000000,
_ = hackney_metrics:histogram_observe(hackney_request_duration_seconds, Labels, DurationSeconds),
_ = hackney_metrics:gauge_dec(hackney_requests_active, Labels),
_ = hackney_metrics:counter_inc(hackney_requests_finished_total, Labels),
{noreply, State};

handle_cast(_Msg, State) ->
Expand All @@ -125,3 +110,11 @@ terminate(_Reason, _State) ->

code_change(_OldVsn, State, _Extra) ->
{ok, State}.

%%====================================================================
%% Internal functions
%%====================================================================

to_binary(Host) when is_binary(Host) -> Host;
to_binary(Host) when is_list(Host) -> list_to_binary(Host);
to_binary(Host) when is_atom(Host) -> atom_to_binary(Host, utf8).
118 changes: 112 additions & 6 deletions src/hackney_metrics.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% Copyright (c) 2012-2018 Benoît Chesneau <benoitc@e-engura.org>
%%% Copyright (c) 2012-2026 Benoît Chesneau <benoitc@e-engura.org>
%%%

-module(hackney_metrics).
Expand All @@ -12,16 +12,122 @@
%% API
-export([
init/0,
get_engine/0
get_backend/0
]).

%% Counter operations
-export([
counter_inc/2,
counter_inc/3
]).

%% Gauge operations
-export([
gauge_set/3,
gauge_inc/2,
gauge_dec/2
]).

%% Histogram operations
-export([
histogram_observe/3
]).

%% Metric declarations
-export([
declare_counter/3,
declare_gauge/3,
declare_histogram/3,
declare_histogram/4,
declare_pool_metrics/1
]).

-include("hackney.hrl").

%% Default duration histogram buckets (in seconds)
-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]).

%% @doc Initialize the metrics system.
%% Determines the backend to use and declares all hackney metrics.
init() ->
Metrics = metrics:init(hackney_util:mod_metrics()),
ets:insert(?CONFIG, {mod_metrics, Metrics}).
Backend = hackney_util:mod_metrics(),
ets:insert(?CONFIG, {metrics_backend, Backend}),
declare_metrics(Backend).

%% @doc Get the current metrics backend module.
get_backend() ->
try
ets:lookup_element(?CONFIG, metrics_backend, 2)
catch
error:badarg ->
%% ETS table not ready yet, return dummy backend
hackney_metrics_dummy
end.

%% @doc Increment a counter by 1.
counter_inc(Name, Labels) ->
(get_backend()):counter_inc(Name, Labels).

%% @doc Increment a counter by a value.
counter_inc(Name, Labels, Value) ->
(get_backend()):counter_inc(Name, Labels, Value).

%% @doc Set a gauge to a value.
gauge_set(Name, Labels, Value) ->
(get_backend()):gauge_set(Name, Labels, Value).

%% @doc Increment a gauge by 1.
gauge_inc(Name, Labels) ->
(get_backend()):gauge_inc(Name, Labels).

%% @doc Decrement a gauge by 1.
gauge_dec(Name, Labels) ->
(get_backend()):gauge_dec(Name, Labels).

%% @doc Observe a value for a histogram.
histogram_observe(Name, Labels, Value) ->
(get_backend()):histogram_observe(Name, Labels, Value).

%% @doc Declare a counter metric.
declare_counter(Name, Help, LabelKeys) ->
(get_backend()):declare_counter(Name, Help, LabelKeys).

%% @doc Declare a gauge metric.
declare_gauge(Name, Help, LabelKeys) ->
(get_backend()):declare_gauge(Name, Help, LabelKeys).

%% @doc Declare a histogram metric with default buckets.
declare_histogram(Name, Help, LabelKeys) ->
(get_backend()):declare_histogram(Name, Help, LabelKeys).

%% @doc Declare a histogram metric with custom buckets.
declare_histogram(Name, Help, LabelKeys, Buckets) ->
(get_backend()):declare_histogram(Name, Help, LabelKeys, Buckets).

%% @doc Declare pool-specific metrics.
%% Called when a new pool is created.
declare_pool_metrics(_PoolName) ->
Backend = get_backend(),
%% Only declare once (idempotent for prometheus)
Backend:declare_gauge(hackney_pool_free_count,
<<"Number of free/available connections in the pool">>, [pool]),
Backend:declare_gauge(hackney_pool_in_use_count,
<<"Number of connections currently in use">>, [pool]),
Backend:declare_counter(hackney_pool_checkouts_total,
<<"Total number of connection checkouts">>, [pool]),
ok.

get_engine() ->
ets:lookup_element(?CONFIG, mod_metrics, 2).
%% @private
%% Declare all hackney metrics at startup.
declare_metrics(Backend) ->
%% Request metrics
Backend:declare_counter(hackney_requests_total,
<<"Total number of HTTP requests started">>, [host]),
Backend:declare_gauge(hackney_requests_active,
<<"Number of currently active HTTP requests">>, [host]),
Backend:declare_counter(hackney_requests_finished_total,
<<"Total number of HTTP requests finished">>, [host]),
Backend:declare_histogram(hackney_request_duration_seconds,
<<"HTTP request duration in seconds">>, [host], ?DEFAULT_DURATION_BUCKETS),
%% Pool metrics are declared when pools are created
ok.
33 changes: 33 additions & 0 deletions src/hackney_metrics_backend.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% Copyright (c) 2012-2026 Benoît Chesneau <benoitc@e-engura.org>
%%%

-module(hackney_metrics_backend).
-author("benoitc").

%% Behaviour callbacks for hackney metrics backends
%%
%% Implementations must export all callback functions.
%% See hackney_metrics_dummy for a reference implementation.

%% Counter operations (monotonically increasing)
-callback counter_inc(Name :: atom(), Labels :: map()) -> ok.
-callback counter_inc(Name :: atom(), Labels :: map(), Value :: number()) -> ok.

%% Gauge operations (can go up or down)
-callback gauge_set(Name :: atom(), Labels :: map(), Value :: number()) -> ok.
-callback gauge_inc(Name :: atom(), Labels :: map()) -> ok.
-callback gauge_dec(Name :: atom(), Labels :: map()) -> ok.

%% Histogram operations (for timing/distribution measurements)
-callback histogram_observe(Name :: atom(), Labels :: map(), Value :: number()) -> ok.

%% Metric lifecycle
-callback declare_counter(Name :: atom(), Help :: binary(), LabelKeys :: [atom()]) -> ok.
-callback declare_gauge(Name :: atom(), Help :: binary(), LabelKeys :: [atom()]) -> ok.
-callback declare_histogram(Name :: atom(), Help :: binary(), LabelKeys :: [atom()]) -> ok.
-callback declare_histogram(Name :: atom(), Help :: binary(), LabelKeys :: [atom()], Buckets :: [number()]) -> ok.
Loading
Loading