blob: e226d6cfcd502a6e8b1a742d220f9a8b05c16910 [file] [log] [blame]
%% @doc A reporter that sends the spans to an jaeger collector using HTTP.
%%
%% To start a reporter process, please use {@link jaeger_passage_reporter:start/1} or {@link jaeger_passage_reporter:start/2}.
%%
%%
%% === Examples ===
%%
%% ```
%% %% Starts `example_reporter'
%% {ok, Reporter} = jaeger_passage_reporter:start(example_reporter, [{protocol, http}]).
%% [example_reporter] = jaeger_passage_reporter:which_reporters().
%%
%% %% Registers `example_tracer'
%% Context = jaeger_passage_span_context.
%% Sampler = passage_sampler_all:new().
%% ok = passage_tracer_registry:register(example_tracer, Context, Sampler, Reporter).
%%
%% %% Starts and finishes a span
%% Span = passage:start_span(example, [{tracer, example_tracer}]).
%%
%% passage:finish_span(Span). % The span will send to the jaeger collector on the localhost
%% '''
-module(jaeger_passage_reporter_http).
-behaviour(gen_server).
-include("constants.hrl").
%%------------------------------------------------------------------------------
%% Exported API
%%------------------------------------------------------------------------------
-export([httpc_client/5]).
-export_type([start_option/0, start_options/0]).
%%------------------------------------------------------------------------------
%% Application Internal API
%%------------------------------------------------------------------------------
-export([start_link/2]).
%%------------------------------------------------------------------------------
%% 'gen_server' Callback API
%%------------------------------------------------------------------------------
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
%%------------------------------------------------------------------------------
%% Macros & Records
%%------------------------------------------------------------------------------
-define(STATE, ?MODULE).
-record(?STATE,
{
endpoint :: string(),
options :: start_options(),
http_client :: http_client(),
default_service_name :: atom(),
process_tags :: passage:tags()
}).
-define(CONTENT_TYPE, {"Content-Type", "application/x-thrift"}).
%%------------------------------------------------------------------------------
%% Exported Types
%%------------------------------------------------------------------------------
-type start_options() :: [start_option()].
%% Options for {@link jaeger_passage_reporter:start/2}.
-type start_option() :: {default_service_name, atom()}
| {process_tags, passage:tags()}
| {endpoint, string()}
| {http_client, http_client()}
| (HttpClientSpecificOption :: any()).
%% <ul>
%% <li><b>default_service_name</b>: The default service name. If a reporting span has `location.application' tag, the value is used as the service name instead of this. The default value is `ReporterId'.</li>
%% <li><b>process_tags</b>: The tags of the reporting process. The default value is `#{}'.</li>
%% <li><b>endpoint</b>: The jaeger endpoint URL for sending thrift messages. The default value is `http://127.0.0.1:14268'.</li>
%% <li><b>http_client</b>: The callback to call to send span to jaeger. {@link httpc_client/5} is used by default.</li>
%% </ul>
%%
%% Example of a `http_client' calback:
%% ```
%% Client = fun(Url, Method, Headers, Body, ReporterOptions) ->
%% User = proplists:get_value(user, ReporterOptions),
%% Password = proplists:get_value(password, ReporterOptions),
%% ibrowse:send_req(Url, Headers, Method, Body, [{basic_auth, {User, Password}}])
%% end.
%% '''
-type http_client() :: fun((
Url :: string(),
Method :: post,
Headers :: [{string(), string()}],
Body :: string() | binary(),
ReporterOptions :: start_options()) ->
ok).
%% Type of HTTP client.
%%------------------------------------------------------------------------------
%% Exported Functions
%%------------------------------------------------------------------------------
%% @doc The default HTTP client based on the standard `httpc' module.
httpc_client(Url, Method, _Headers, Body, _ReporterOptions) ->
httpc:request(Method, {Url, [], "application/x-thrift", Body}, [], []),
ok.
%%------------------------------------------------------------------------------
%% Application Internal Functions
%%------------------------------------------------------------------------------
%% @private
-spec start_link(jaeger_passage_reporter:reporter_id(), start_options()) -> {ok, pid()} | {error, Reason} when
Reason :: {already_started, pid()} | term().
start_link(ReporterId, Options) ->
Name = jaeger_passage_local_ns:reporter_name(ReporterId),
gen_server:start_link(Name, ?MODULE, {ReporterId, Options}, []).
%%------------------------------------------------------------------------------
%% 'gen_server' Callback Functions
%%------------------------------------------------------------------------------
%% @private
init({ReporterId, Options}) ->
Endpoint = proplists:get_value(endpoint, Options, "http://127.0.0.1:14268"),
EndpointURL = Endpoint ++ "/api/traces",
HttpClient = proplists:get_value(http_client, Options, fun httpc_client/5),
is_function(HttpClient, 5) orelse error(badarg, [ReporterId, Options]),
DefaultServiceName = proplists:get_value(default_service_name, Options, ReporterId),
Tags0 = proplists:get_value(process_tags, Options, #{}),
{ok, Hostname} = inet:gethostname(),
{ok, Version} = application:get_key(vsn),
Tags1 =
maps:merge(
Tags0,
#{
?JAEGER_CLIENT_VERSION_TAG_KEY => list_to_binary(["jaeger_passage-", Version]),
?TRACER_HOSTNAME_TAG_KEY => list_to_binary(Hostname),
'erlang.node' => node()
}),
State =
#?STATE{
endpoint = EndpointURL,
http_client = HttpClient,
options = Options,
default_service_name = DefaultServiceName,
process_tags = Tags1
},
{ok, State}.
%% @private
handle_call(_Request, _From, State) ->
{noreply, State}.
%% @private
handle_cast({report, Span}, State) ->
handle_report(Span, State);
handle_cast(_Request, State) ->
{noreply, State}.
%% @private
handle_info(_Info, State) ->
{noreply, State}.
%% @private
terminate(_Reason, _State) ->
ok.
%% @private
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal Functions
%%------------------------------------------------------------------------------
-spec handle_report(passage_span:span(), #?STATE{}) -> {noreply, #?STATE{}}.
handle_report(Span, State = #?STATE{default_service_name = DefaultName, process_tags = Tags, endpoint = URI, http_client = HttpClient, options = Options}) ->
Name = maps:get('location.application', passage_span:get_tags(Span), DefaultName),
Message = jaeger_passage_thrift:make_batch(Name, Tags, [Span]),
Encoded = thrift_protocol:encode_struct(Message, binary),
Headers = [?CONTENT_TYPE],
HttpClient(URI, post, Headers, Encoded, Options),
{noreply, State}.