Skip to content

Commit 7130f1f

Browse files
committed
Depend on telemetry and emit produce request event
Add a dependency on the telemetry library and a brod_metrics module confining all telemetry calls, based on the pattern established in [kafka4beam/wolff](https://github.com/kafka4beam/wolff/blob/4feffa6534a960377507fb2dc840db07548b2533/src/wolff_metrics.erl). The first event, `[brod, product_request_sent]`, is emitted by brod_producer for every produce request successfully sent on the wire. This makes the actual wire-level batching observable, which is otherwise invisible to callers More events can be added to brod_metrics incrementally. Related: kafka4beam#503, kafka4beam#512
1 parent 98d6bf4 commit 7130f1f

7 files changed

Lines changed: 116 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
because the request no longer includes a per-request retention time. Use
77
the broker-side `offsets.retention.minutes` setting instead. Negotiated
88
versions v2 through v4 continue to use `offset_retention_seconds`.
9+
- Emit [`telemetry`](https://github.com/beam-telemetry/telemetry) events,
10+
starting with `[brod, produce_request_sent]`.
11+
Events are documented in the new `brod_metrics` module.
12+
This adds a dependency on the `telemetry` library.
913

1014
- 4.6.0
1115
- Add KIP-345 **static group membership** for coordinators configured with an

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ start(ClientId) ->
434434
_CallbackInitArg = []).
435435
```
436436

437+
## Beam Telemetry Hooks
438+
439+
Brod emits [Beam Telemetry](https://github.com/beam-telemetry/telemetry) events that users can attach handler functions to.
440+
The emitted events are documented in the `brod_metrics` module.
441+
437442
## Authentication support
438443

439444
brod supports SASL `PLAIN`, `SCRAM-SHA-256` and `SCRAM-SHA-512` authentication mechanisms out of the box.

rebar.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{deps, [{kafka_protocol, "4.3.5"}]}.
1+
{deps, [{kafka_protocol, "4.3.5"}, {telemetry, "1.4.2"}]}.
22
{project_plugins, [{rebar3_lint, "~> 3.2.5"}]}.
33
{edoc_opts, [{preprocess, true}]}.
44
{erl_opts, [warnings_as_errors, warn_unused_vars,warn_shadow_vars,warn_obsolete_guard,debug_info]}.

src/brod.app.src

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[{description,"Apache Kafka Erlang client library"},
44
{vsn,"git"},
55
{registered,[]},
6-
{applications,[kernel,stdlib,kafka_protocol]},
6+
{applications,[kernel,stdlib,kafka_protocol,telemetry]},
77
{env,[]},
88
{mod, {brod, []}},
99
{modules,[]},

src/brod_metrics.erl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
%%%
2+
%%% Copyright (c) 2026 kafka4beam contributors
3+
%%%
4+
%%% Licensed under the Apache License, Version 2.0 (the "License");
5+
%%% you may not use this file except in compliance with the License.
6+
%%% You may obtain a copy of the License at
7+
%%%
8+
%%% http://www.apache.org/licenses/LICENSE-2.0
9+
%%%
10+
%%% Unless required by applicable law or agreed to in writing, software
11+
%%% distributed under the License is distributed on an "AS IS" BASIS,
12+
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
%%% See the License for the specific language governing permissions and
14+
%%% limitations under the License.
15+
%%%
16+
17+
%% @doc Telemetry events emitted by brod.
18+
%%
19+
%% Brod emits events using the
20+
%% <a href="https://github.com/beam-telemetry/telemetry">Beam Telemetry</a>
21+
%% library. Users can attach handler functions to these events, for
22+
%% example to report metrics. Each event is emitted by a function in
23+
%% this module; see the function documentation for the measurements
24+
%% and metadata attached to each event.
25+
-module(brod_metrics).
26+
27+
-export([produce_request_sent/3]).
28+
29+
-type batch_input() :: brod:batch_input().
30+
-type partition() :: brod:partition().
31+
-type topic() :: brod:topic().
32+
33+
%% @doc Emit a `[brod, produce_request_sent]' event.
34+
%%
35+
%% Emitted by producers for each produce request successfully sent on
36+
%% wire, i.e. one event per Kafka produce request (message batch).
37+
%%
38+
%% Measurements:
39+
%% <ul>
40+
%% <li>`count': number of messages in the request.</li>
41+
%% <li>`bytes': total size of keys, values and headers of the messages
42+
%% in the request (before encoding and compression).</li>
43+
%% </ul>
44+
%%
45+
%% Metadata: `topic' and `partition' the request was sent to.
46+
-spec produce_request_sent(topic(), partition(), batch_input()) -> ok.
47+
produce_request_sent(Topic, Partition, BatchInput) ->
48+
telemetry:execute([brod, produce_request_sent],
49+
#{ count => length(BatchInput)
50+
, bytes => brod_utils:bytes(BatchInput)
51+
},
52+
#{ topic => Topic
53+
, partition => Partition
54+
}).
55+
56+
%%%_* Emacs ====================================================================
57+
%%% Local Variables:
58+
%%% allout-layout: t
59+
%%% erlang-indent-level: 2
60+
%%% End:

src/brod_producer.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,12 @@ do_send_fun(ExtraArg, Conn, BatchInput, Vsn) ->
442442
brod_kafka_request:produce(Vsn, Topic, Partition, BatchInput,
443443
RequiredAcks, AckTimeout, Compression),
444444
case send(Conn, ProduceRequest) of
445-
ok when ProduceRequest#kpro_req.no_ack ->
446-
ok;
447445
ok ->
448-
{ok, ProduceRequest#kpro_req.ref};
446+
brod_metrics:produce_request_sent(Topic, Partition, BatchInput),
447+
case ProduceRequest#kpro_req.no_ack of
448+
true -> ok;
449+
false -> {ok, ProduceRequest#kpro_req.ref}
450+
end;
449451
{error, Reason} ->
450452
{error, Reason}
451453
end.

test/brod_producer_stub_SUITE.erl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@
2929
%% Test cases
3030
-export([ t_normal_flow/1
3131
, t_no_required_acks/1
32+
, t_produce_request_telemetry/1
3233
, t_retry_on_same_connection/1
3334
, t_retry_on_kafka_storage_error/1
3435
, t_connection_down_retry/1
3536
, t_leader_migration/1
3637
]).
3738

39+
%% Telemetry test handler
40+
-export([ handle_telemetry_event/4 ]).
41+
3842

3943
-include_lib("eunit/include/eunit.hrl").
4044
-include("brod_int.hrl").
@@ -168,6 +172,39 @@ t_no_required_acks(Config) when is_list(Config) ->
168172
result = brod_produce_req_acked}, ok, 2000),
169173
ok = brod_producer:stop(Producer).
170174

175+
t_produce_request_telemetry(Config) when is_list(Config) ->
176+
Tester = self(),
177+
HandlerId = <<"t-produce-request-telemetry">>,
178+
ok = telemetry:attach(HandlerId, [brod, produce_request_sent],
179+
fun ?MODULE:handle_telemetry_event/4,
180+
#{pid => Tester}),
181+
try
182+
meck:expect(brod_client, get_leader_connection,
183+
fun(_, <<"topic">>, 0) -> {ok, Tester} end),
184+
meck:expect(kpro, request_async,
185+
fun(Connection, KafkaReq) ->
186+
Connection ! {request_async, KafkaReq},
187+
ok
188+
end),
189+
ProducerConfig = [{max_linger_ms, 1000},
190+
{max_linger_count, 3}],
191+
{ok, Producer} = brod_producer:start_link(client, <<"topic">>, 0,
192+
ProducerConfig),
193+
{ok, _} = brod_producer:produce(Producer, <<"k1">>, <<"v1">>),
194+
{ok, _} = brod_producer:produce(Producer, <<"k2">>, <<"v2">>),
195+
{ok, _} = brod_producer:produce(Producer, <<"k3">>, <<"v3">>),
196+
?WAIT({request_async, _}, ok, 2000),
197+
?WAIT({telemetry, [brod, produce_request_sent], Measurements, Metadata},
198+
begin
199+
?assertMatch(#{count := 3, bytes := Bytes} when Bytes > 0,
200+
Measurements),
201+
?assertEqual(#{topic => <<"topic">>, partition => 0}, Metadata)
202+
end, 2000),
203+
ok = brod_producer:stop(Producer)
204+
after
205+
telemetry:detach(HandlerId)
206+
end.
207+
171208
t_retry_on_same_connection(Config) when is_list(Config) ->
172209
Tester = self(),
173210
%% A mocked connection process which expects 2 requests to be sent
@@ -413,6 +450,9 @@ t_leader_migration(Config) when is_list(Config) ->
413450

414451
%%%_* Help functions ===========================================================
415452

453+
handle_telemetry_event(Event, Measurements, Metadata, #{pid := Pid}) ->
454+
Pid ! {telemetry, Event, Measurements, Metadata}.
455+
416456
meck_module(Module) ->
417457
meck:new(Module, [passthrough, no_passthrough_cover, no_history]).
418458

0 commit comments

Comments
 (0)