Merge pull request #3 from iilyak/add-http-reporter

Add HTTP reporter
diff --git a/README.md b/README.md
index ddfd2a6..078eee6 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,23 @@
 $ firefox http://localhost:16686/
 ```
 
+Selecting reporter
+------------------
+
+By default 'compact' jaeger.thrift over UDP reporter is used. However it is
+possible to select different reporter. Bellow is a configuration matrics for
+available options:
+
+| protocol | thrift_protocol | jaeger port | description                      |
+|----------|-----------------|-------------|----------------------------------|
+| udp      | compact         | 6831        | accept jaeger.thrift over compact thrift protocol (default) |
+| udp      | binary          | 6832        | accept jaeger.thrift over binary thrift protocol |
+| http     | N/A             | 14268       | accept jaeger.thrift directly from clients |
+
+The HTTP version is beneficial if you don't have jaeger agents deployed or your
+spans are greater than max udp packet size (65Kb).
+Otherwise it is better to use default.
+
 References
 -----------
 
@@ -52,3 +69,4 @@
 - [Jaeger](https://uber.github.io/jaeger/)
 - [jaeger-client-go/README.md](https://github.com/jaegertracing/jaeger-client-go/blob/v2.9.0/README.md)
 - [Jaeger Client Library](https://github.com/jaegertracing/jaeger/blob/master/docs/client_libraries.md)
+- [Jaeger default ports](https://www.jaegertracing.io/docs/1.8/getting-started/)
\ No newline at end of file
diff --git a/rebar.config.script b/rebar.config.script
index 4454cdc..74eb314 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -5,7 +5,7 @@
   [
    {local, ".*", {git, "https://github.com/sile/local.git", {tag, "0.2.1"}}},
    {passage, ".*", {git, "https://github.com/sile/passage.git", {tag, "0.2.6"}}},
-   {thrift_protocol, ".*", {git, "https://github.com/sile/thrift_protocol.git", {tag, "0.1.3"}}}
+   {thrift_protocol, ".*", {git, "https://github.com/sile/thrift_protocol.git", {tag, "0.1.5"}}}
   ],
 
 case IsRebar3 of
diff --git a/rebar.lock b/rebar.lock
index c9ba4ec..9d5110a 100644
--- a/rebar.lock
+++ b/rebar.lock
@@ -1,10 +1,10 @@
 {"1.1.0",
 [{<<"local">>,{pkg,<<"local">>,<<"0.2.1">>},0},
  {<<"passage">>,{pkg,<<"passage">>,<<"0.2.6">>},0},
- {<<"thrift_protocol">>,{pkg,<<"thrift_protocol">>,<<"0.1.3">>},0}]}.
+ {<<"thrift_protocol">>,{pkg,<<"thrift_protocol">>,<<"0.1.5">>},0}]}.
 [
 {pkg_hash,[
  {<<"local">>, <<"F82483CD6DB6A39B0E4C59B37C2FCCCF5B96D90A746AFC3D79A81D31E3D40963">>},
  {<<"passage">>, <<"7B0A6F0A6806B056DC3323A6A0243503642E6425F45E33B87277EA0BE88BD130">>},
- {<<"thrift_protocol">>, <<"CDD0C06BFC235159D789353CBB4F01F9FF04592F30329CB3DF2C77E28D92CCC0">>}]}
+ {<<"thrift_protocol">>, <<"300DB7CA06BED397406A4680AD3DD3A0212AC7BEABDC81FA03C3C3DADEA13673">>}]}
 ].
diff --git a/src/jaeger_passage.erl b/src/jaeger_passage.erl
index 50c5aa5..cf27120 100644
--- a/src/jaeger_passage.erl
+++ b/src/jaeger_passage.erl
@@ -33,7 +33,7 @@
       Tracer :: passage:tracer_id(),
       Sampler :: passage_sampler:sampler().
 start_tracer(Tracer, Sampler) ->
-    start_tracer(Tracer, Sampler, []).
+    start_tracer(Tracer, Sampler, [{protocol, udp}]).
 
 %% @doc Starts a tracer for Jaeger.
 %%
diff --git a/src/jaeger_passage_reporter.erl b/src/jaeger_passage_reporter.erl
index 32f537e..1287f48 100644
--- a/src/jaeger_passage_reporter.erl
+++ b/src/jaeger_passage_reporter.erl
@@ -29,7 +29,6 @@
 -module(jaeger_passage_reporter).
 
 -behaviour(passage_reporter).
--behaviour(gen_server).
 
 -include("constants.hrl").
 
@@ -45,36 +44,11 @@
 -export_type([start_option/0, start_options/0]).
 
 %%------------------------------------------------------------------------------
-%% Application Internal API
-%%------------------------------------------------------------------------------
--export([start_link/2]).
-
-%%------------------------------------------------------------------------------
 %% 'passage_reporter' Callback API
 %%------------------------------------------------------------------------------
 -export([report/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,
-        {
-          socket               :: gen_udp:socket(),
-          thrift_format        :: thrift_protocol:format(),
-          agent_host           :: inet:hostname(),
-          agent_port           :: inet:port_number(),
-          default_service_name :: atom(),
-          process_tags         :: passage:tags()
-        }).
-
-%%------------------------------------------------------------------------------
 %% Exported Types
 %%------------------------------------------------------------------------------
 -type reporter_id() :: atom().
@@ -83,28 +57,28 @@
 -type start_options() :: [start_option()].
 %% Options for {@link start/2}.
 
--type start_option() :: {thrift_format, thrift_protocol:format()}
-                      | {agent_host, inet:hostname()}
-                      | {agent_port, inet:port_number()}
+-type start_option() :: jaeger_passage_reporter_udp:start_option()
+                      | jaeger_passage_reporter_http:start_option()
+                      | {protocol, udp | http}
                       | {default_service_name, atom()}
                       | {process_tags, passage:tags()}.
+
+%% Common reporter options
+%% <ul>
+%%   <li><b>protocol</b>: Communication protocol used to connect to jaeger. The value is used to select reporter module. Possible values are: `udp' | `http'. The default value is `udp'.</li>
+%%   <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>
+%% </ul>
+%% UDP reporter specific options
 %% <ul>
 %%   <li><b>thrift_format</b>: The format for encoding thrift messages. The default value is `compact'.</li>
 %%   <li><b>agent_host</b>: The hostname of the jaeger agent. The default value is `"127.0.0.1"'.</li>
 %%   <li><b>agent_port</b>: The port of the jaeger agent. The default values for the thrift format `compact' and `binary' are `6831' and `6832' respectively.</li>
-%%   <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>
 %% </ul>
-
-%%------------------------------------------------------------------------------
-%% Application Internal Functions
-%%------------------------------------------------------------------------------
-%% @private
--spec start_link(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}, []).
+%% HTTP reporter specific options
+%% <ul>
+%%   <li><b>endpoint</b>: The jaeger endpoint URL for sending thrift messages. The default value is `http://127.0.0.1:14268'.</li>
+%% </ul>
 
 %%------------------------------------------------------------------------------
 %% Exported Functions
@@ -113,7 +87,7 @@
 -spec start(reporter_id()) -> {ok, passage_reporter:reporter()} | {error, Reason} when
       Reason :: {already_started, pid()} | term().
 start(ReporterId) ->
-    start(ReporterId, []).
+    start(ReporterId, [{protocol, udp}]).
 
 %% @doc Starts a reporter process.
 -spec start(reporter_id(), start_options()) -> {ok, Reporter} | {error, Reason} when
@@ -123,8 +97,13 @@
     Args = [ReporterId, Options],
     is_atom(ReporterId) orelse error(badarg, Args),
     is_list(Options) orelse error(badarg, Args),
-
-    case jaeger_passage_reporter_sup:start_child(ReporterId, Options) of
+    ReporterModule = case proplists:get_value(protocol, Options, udp) of
+        udp -> jaeger_passage_reporter_udp;
+        http -> jaeger_passage_reporter_http;
+        _ -> error(badarg, Args)
+    end,
+    ReporterOptions = proplists:delete(protocol, Options),
+    case jaeger_passage_reporter_sup:start_child(ReporterId, ReporterModule, ReporterOptions) of
         {error, Reason} -> {error, Reason};
         {ok, _Pid}      -> {ok, passage_reporter:new(?MODULE, ReporterId)}
     end.
@@ -158,74 +137,3 @@
 report(ReporterId, Span) ->
     Server = jaeger_passage_local_ns:reporter_name(ReporterId),
     gen_server:cast(Server, {report, Span}).
-
-%%------------------------------------------------------------------------------
-%% 'gen_server' Callback Functions
-%%------------------------------------------------------------------------------
-%% @private
-init({ReporterId, Options}) ->
-    Format = proplists:get_value(thrift_format, Options, compact),
-    DefaultPort =
-        case Format of
-            compact -> 6831;
-            binary  -> 6832
-        end,
-    AgentHost = proplists:get_value(agent_host, Options, "127.0.0.1"),
-    AgentPort = proplists:get_value(agent_port, Options, DefaultPort),
-    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()
-           }),
-    {ok, Socket} = gen_udp:open(0),
-    State =
-        #?STATE{
-            socket        = Socket,
-            thrift_format = Format,
-            agent_host    = AgentHost,
-            agent_port    = AgentPort,
-            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}) ->
-    Name = maps:get('location.application', passage_span:get_tags(Span), DefaultName),
-    Message = jaeger_passage_thrift:make_emit_batch_message(Name, Tags, [Span]),
-    Encoded = thrift_protocol:encode_message(Message, State#?STATE.thrift_format),
-    ok = gen_udp:send(State#?STATE.socket, State#?STATE.agent_host, State#?STATE.agent_port, Encoded),
-    {noreply, State}.
diff --git a/src/jaeger_passage_reporter_http.erl b/src/jaeger_passage_reporter_http.erl
new file mode 100644
index 0000000..0b0097c
--- /dev/null
+++ b/src/jaeger_passage_reporter_http.erl
@@ -0,0 +1,185 @@
+%% @doc A reporter that sends the spans to an jaeger agent
+%%
+%% === 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 agent on the localhost
+%% '''
+%%
+%% === Refereces ===
+%%
+%% <ul>
+%% <li><a href="http://jaeger.readthedocs.io/en/latest/architecture/#agent">Jaeger - Architecture - Agent</a></li>
+%% <li><a href="http://jaeger.readthedocs.io/en/latest/deployment/#agent">Jaeger - Deployment - Agent</a></li>
+%% </ul>
+-module(jaeger_passage_reporter_http).
+
+-behaviour(gen_server).
+
+-include("constants.hrl").
+
+%%------------------------------------------------------------------------------
+%% Exported API
+%%------------------------------------------------------------------------------
+-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 start/2}.
+
+-type start_option() :: {endpoint, string()}
+                      | {http_client, http_client()}
+                      | {default_service_name, atom()}
+                      | {process_tags, passage:tags()}.
+%% <ul>
+%%   <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. The httpc client is used by default.</li>
+%%   <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>
+%% </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).
+
+%%------------------------------------------------------------------------------
+%% 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}.
+
+-spec httpc_client(
+        Url    :: string(),
+        Method :: post,
+        Headers :: [{string(), string()}],
+        Body :: string() | binary(),
+        ReporterOptions :: start_options()) ->
+    ok.
+
+httpc_client(Url, Method, _Headers, Body, _ReporterOptions) ->
+    httpc:request(Method, {Url, [], "application/x-thrift", Body}, [], []),
+    ok.
diff --git a/src/jaeger_passage_reporter_sup.erl b/src/jaeger_passage_reporter_sup.erl
index 529fde3..77f23ba 100644
--- a/src/jaeger_passage_reporter_sup.erl
+++ b/src/jaeger_passage_reporter_sup.erl
@@ -9,7 +9,7 @@
 %% Application Internal API
 %%------------------------------------------------------------------------------
 -export([start_link/0]).
--export([start_child/2]).
+-export([start_child/3]).
 -export([stop_child/1]).
 -export([which_children/0]).
 
@@ -25,14 +25,15 @@
 start_link() ->
     supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
--spec start_child(ReporterId, Options) -> {ok, pid()} | {error, Reason} when
+-spec start_child(ReporterId, ReporterModule, Options) -> {ok, pid()} | {error, Reason} when
       ReporterId :: jaeger_passage_reporter:reporter_id(),
+      ReporterModule :: module(),
       Options :: jaeger_passage_reporter:start_options(),
       Reason :: {already_started, pid()} | term().
-start_child(ReporterId, Options) ->
+start_child(ReporterId, ReporterModule, Options) ->
     Child = #{
       id      => ReporterId,
-      start   => {jaeger_passage_reporter, start_link, [ReporterId, Options]},
+      start   => {ReporterModule, start_link, [ReporterId, Options]},
       restart => permanent
      },
     supervisor:start_child(?MODULE, Child).
diff --git a/src/jaeger_passage_reporter_udp.erl b/src/jaeger_passage_reporter_udp.erl
new file mode 100644
index 0000000..485c258
--- /dev/null
+++ b/src/jaeger_passage_reporter_udp.erl
@@ -0,0 +1,160 @@
+%% @doc A reporter that sends the spans to an jaeger agent
+%%
+%% === Examples ===
+%%
+%% ```
+%% %% Starts `example_reporter'
+%% {ok, Reporter} = jaeger_passage_reporter:start(example_reporter, [{protocol, udp}]).
+%% [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 agent on the localhost
+%% '''
+%%
+%% === Refereces ===
+%%
+%% <ul>
+%% <li><a href="http://jaeger.readthedocs.io/en/latest/architecture/#agent">Jaeger - Architecture - Agent</a></li>
+%% <li><a href="http://jaeger.readthedocs.io/en/latest/deployment/#agent">Jaeger - Deployment - Agent</a></li>
+%% </ul>
+-module(jaeger_passage_reporter_udp).
+
+-behaviour(gen_server).
+
+-include("constants.hrl").
+
+%%------------------------------------------------------------------------------
+%% Exported API
+%%------------------------------------------------------------------------------
+-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,
+        {
+          socket               :: gen_udp:socket(),
+          thrift_format        :: thrift_protocol:format(),
+          agent_host           :: inet:hostname(),
+          agent_port           :: inet:port_number(),
+          default_service_name :: atom(),
+          process_tags         :: passage:tags()
+        }).
+
+%%------------------------------------------------------------------------------
+%% Exported Types
+%%------------------------------------------------------------------------------
+
+-type start_options() :: [start_option()].
+%% Options for {@link start/2}.
+
+-type start_option() :: {thrift_format, thrift_protocol:format()}
+                      | {agent_host, inet:hostname()}
+                      | {agent_port, inet:port_number()}.
+%% <ul>
+%%   <li><b>thrift_format</b>: The format for encoding thrift messages. The default value is `compact'.</li>
+%%   <li><b>agent_host</b>: The hostname of the jaeger agent. The default value is `"127.0.0.1"'.</li>
+%%   <li><b>agent_port</b>: The port of the jaeger agent. The default values for the thrift format `compact' and `binary' are `6831' and `6832' respectively.</li>
+%%   <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>
+%% </ul>
+
+%%------------------------------------------------------------------------------
+%% 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}) ->
+    Format = proplists:get_value(thrift_format, Options, compact),
+    DefaultPort =
+        case Format of
+            compact -> 6831;
+            binary  -> 6832
+        end,
+    AgentHost = proplists:get_value(agent_host, Options, "127.0.0.1"),
+    AgentPort = proplists:get_value(agent_port, Options, DefaultPort),
+    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()
+           }),
+    {ok, Socket} = gen_udp:open(0),
+    State =
+        #?STATE{
+            socket        = Socket,
+            thrift_format = Format,
+            agent_host    = AgentHost,
+            agent_port    = AgentPort,
+            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}) ->
+    Name = maps:get('location.application', passage_span:get_tags(Span), DefaultName),
+    Message = jaeger_passage_thrift:make_emit_batch_message(Name, Tags, [Span]),
+    Encoded = thrift_protocol:encode_message(Message, State#?STATE.thrift_format),
+    ok = gen_udp:send(State#?STATE.socket, State#?STATE.agent_host, State#?STATE.agent_port, Encoded),
+    {noreply, State}.
diff --git a/src/jaeger_passage_thrift.erl b/src/jaeger_passage_thrift.erl
index 28537fd..d8d81da 100644
--- a/src/jaeger_passage_thrift.erl
+++ b/src/jaeger_passage_thrift.erl
@@ -15,7 +15,7 @@
 %%------------------------------------------------------------------------------
 %% Application Internal API
 %%------------------------------------------------------------------------------
--export([make_emit_batch_message/3]).
+-export([make_emit_batch_message/3, make_batch/3]).
 
 %%------------------------------------------------------------------------------
 %% Macros
@@ -49,6 +49,12 @@
        body         = ?STRUCT(Batch)
       }.
 
+-spec make_batch(atom(), passage:tags(), [passage_span:span()]) ->
+                                     thrift_protocol:struct().
+make_batch(ServiceName, ServiceTags, Spans) ->
+    Process = make_process(ServiceName, ServiceTags),
+    ?STRUCT(Process, make_spans(Spans)).
+
 %%------------------------------------------------------------------------------
 %% Internal Functions
 %%------------------------------------------------------------------------------
diff --git a/test/jaeger_passage_repoter_tests.erl b/test/jaeger_passage_repoter_tests.erl
index 7aa1dd0..cdc25a5 100644
--- a/test/jaeger_passage_repoter_tests.erl
+++ b/test/jaeger_passage_repoter_tests.erl
@@ -6,27 +6,73 @@
 %%------------------------------------------------------------------------------
 %% Test Cases
 %%------------------------------------------------------------------------------
-basic_test() ->
+basic_udp_test() ->
     {ok, _} = application:ensure_all_started(jaeger_passage),
 
-    %% Starts `test_reporter'
-    {ok, Reporter} = jaeger_passage_reporter:start(test_reporter),
-    [test_reporter] = jaeger_passage_reporter:which_reporters(),
+    %% Starts `udp_reporter'
+    {ok, Reporter} = jaeger_passage_reporter:start(udp_reporter),
+    [udp_reporter] = jaeger_passage_reporter:which_reporters(),
 
     %% Registers `test_tracer'
     Context = jaeger_passage_span_context,
     Sampler = passage_sampler_all:new(),
-    ok = passage_tracer_registry:register(test_tracer, Context, Sampler, Reporter),
+    ok = passage_tracer_registry:register(udp_tracer, Context, Sampler, Reporter),
 
     %% Starts and finishes spans
-    passage_pd:start_span(test_root, [{tracer, test_tracer}]),
+    passage_pd:start_span(test_root, [{tracer, udp_tracer}]),
     passage_pd:start_span(test_child),
     passage_pd:log(#{message => "Hello World"}),
     passage_pd:finish_span(),
     passage_pd:finish_span(),
     timer:sleep(50),
 
-    ok = jaeger_passage_reporter:stop(test_reporter),
+    ok = jaeger_passage_reporter:stop(udp_reporter),
     [] = jaeger_passage_reporter:which_reporters(),
 
     ok = application:stop(jaeger_passage).
+
+basic_http_test() ->
+    {ok, _} = application:ensure_all_started(jaeger_passage),
+
+    %% Starts `http_reporter'
+    {ok, Reporter} = jaeger_passage_reporter:start(http_reporter, [
+        {protocol, http},
+        {http_client, fun http_client/5}
+    ]),
+    [http_reporter] = jaeger_passage_reporter:which_reporters(),
+
+    %% Registers `test_tracer'
+    Context = jaeger_passage_span_context,
+    Sampler = passage_sampler_all:new(),
+    ok = passage_tracer_registry:register(http_tracer, Context, Sampler, Reporter),
+
+    %% Starts and finishes spans
+    passage_pd:start_span(test_root, [{tracer, http_tracer}]),
+    passage_pd:start_span(test_child),
+    passage_pd:log(#{message => "Hello World"}),
+    passage_pd:finish_span(),
+    passage_pd:finish_span(),
+    timer:sleep(50),
+
+    ok = jaeger_passage_reporter:stop(http_reporter),
+    [] = jaeger_passage_reporter:which_reporters(),
+
+    ok = application:stop(jaeger_passage).
+
+error_http_test() ->
+    {ok, _} = application:ensure_all_started(jaeger_passage),
+
+    %% Starts `http_reporter'
+    ?assertMatch({error, {{badarg, _}, _}}, jaeger_passage_reporter:start(http_reporter, [
+        {protocol, http},
+        {http_client, undefined}
+    ])),
+
+    %% Starts `http_reporter'
+    ?assertError(badarg, jaeger_passage_reporter:start(http_reporter, [
+        {protocol, undefined}
+    ])).
+
+
+http_client(_URI, _Method, _Headers, _Encoded, _Options) ->
+    ok.
\ No newline at end of file