Skip to content

Commit e32a6bb

Browse files
authored
Merge pull request #197 from tompave/telemetry
Add support for Telemetry events
2 parents f1808b3 + de91ad3 commit e32a6bb

File tree

13 files changed

+967
-21
lines changed

13 files changed

+967
-21
lines changed

.iex.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,5 @@ cacheinfo = fn() ->
5656
IO.puts "size: #{size}"
5757
:ets.i(:fun_with_flags_cache)
5858
end
59+
60+
# FunWithFlags.Telemetry.attach_debug_handler()

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
* Add support for Elixir 1.18. Drop support for Elixir 1.15. Elixir >= 1.16 is now required. Dropping support for older versions of Elixir simply means that this package is no longer tested with them in CI, and that compatibility issues are not considered bugs.
66
* Drop support for Erlang/OTP 24, and Erlang/OTP >= 25 is now required. Dropping support for older versions of Erlang/OTP simply means that this package is not tested with them in CI, and that no compatibility issues are considered bugs.
7+
* Instrument the package with [Telemetry](https://hex.pm/packages/telemetry): FunWithFlags now emits Telemetry events for persistence operations. ([pull/197](https://github.com/tompave/fun_with_flags/pull/197), and thanks [Kasse-Dembele](https://github.com/Kasse-Dembele) for suggesting the feature and sharing his work in [pull/176](https://github.com/tompave/fun_with_flags/pull/176))
78
* Improve how the change-notification Phoenix.PubSub adapter manages its connection and readiness status. ([pull/191](https://github.com/tompave/fun_with_flags/pull/191))
8-
* Adding a suite of synthetic benchmark scripts for the package. ([pull/193](https://github.com/tompave/fun_with_flags/pull/193))
9+
* Add a suite of synthetic benchmark scripts for the package. ([pull/193](https://github.com/tompave/fun_with_flags/pull/193))
910

1011
## v1.12.0
1112

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ It stores flag information in Redis or a relational DB (PostgreSQL, MySQL, or SQ
4242
- [PubSub Adapters](#pubsub-adapters)
4343
* [Extensibility](#extensibility)
4444
- [Custom Persistence Adapters](#custom-persistence-adapters)
45+
* [Telemetry](#telemetry)
4546
* [Application Start Behaviour](#application-start-behaviour)
4647
* [Testing](#testing)
4748
* [Development](#development)
@@ -705,6 +706,11 @@ And then configure the library to use it:
705706
```elixir
706707
config :fun_with_flags, :persistence, adapter: MyApp.MyAlternativeFlagStore
707708
```
709+
## Telemetry
710+
711+
FunWithFlags is instrumented with [Telemetry](https://hex.pm/packages/telemetry) and emits events at runtime. Please refer to the [Telemetry docs](https://hexdocs.pm/telemetry/readme.html) for detailed instructions on how to consume the emitted events.
712+
713+
The full list of events emitted by FunWithFlags are documented in the [FunWithFlags.Telemetry](https://hexdocs.pm/fun_with_flags/FunWithFlags.Telemetry.html) module.
708714

709715
## Application Start Behaviour
710716

lib/fun_with_flags.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ defmodule FunWithFlags do
469469
"""
470470
@spec all_flag_names() :: {:ok, [atom]} | {:error, any}
471471
def all_flag_names do
472-
Config.persistence_adapter().all_flag_names()
472+
@store.all_flag_names()
473473
end
474474

475475
@doc """
@@ -483,7 +483,7 @@ defmodule FunWithFlags do
483483
"""
484484
@spec all_flags() :: {:ok, [FunWithFlags.Flag.t]} | {:error, any}
485485
def all_flags do
486-
Config.persistence_adapter().all_flags()
486+
@store.all_flags()
487487
end
488488

489489

@@ -498,7 +498,11 @@ defmodule FunWithFlags do
498498
case all_flag_names() do
499499
{:ok, names} ->
500500
if name in names do
501-
case Config.persistence_adapter().get(name) do
501+
result =
502+
Config.persistence_adapter().get(name)
503+
|> FunWithFlags.Telemetry.emit_persistence_event(:read, name, nil)
504+
505+
case result do
502506
{:ok, flag} -> flag
503507
error -> error
504508
end

lib/fun_with_flags/simple_store.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ defmodule FunWithFlags.SimpleStore do
22
@moduledoc false
33

44
import FunWithFlags.Config, only: [persistence_adapter: 0]
5+
alias FunWithFlags.Telemetry
56

67
@spec lookup(atom) :: {:ok, FunWithFlags.Flag.t}
78
def lookup(flag_name) do
8-
case persistence_adapter().get(flag_name) do
9+
result =
10+
persistence_adapter().get(flag_name)
11+
|> Telemetry.emit_persistence_event(:read, flag_name, nil)
12+
13+
case result do
914
{:ok, flag} -> {:ok, flag}
1015
_ -> raise "Can't load feature flag"
1116
end
@@ -14,25 +19,30 @@ defmodule FunWithFlags.SimpleStore do
1419
@spec put(atom, FunWithFlags.Gate.t) :: {:ok, FunWithFlags.Flag.t} | {:error, any()}
1520
def put(flag_name, gate) do
1621
persistence_adapter().put(flag_name, gate)
22+
|> Telemetry.emit_persistence_event(:write, flag_name, gate)
1723
end
1824

1925
@spec delete(atom, FunWithFlags.Gate.t) :: {:ok, FunWithFlags.Flag.t} | {:error, any()}
2026
def delete(flag_name, gate) do
2127
persistence_adapter().delete(flag_name, gate)
28+
|> Telemetry.emit_persistence_event(:delete_gate, flag_name, gate)
2229
end
2330

2431
@spec delete(atom) :: {:ok, FunWithFlags.Flag.t} | {:error, any()}
2532
def delete(flag_name) do
2633
persistence_adapter().delete(flag_name)
34+
|> Telemetry.emit_persistence_event(:delete_flag, flag_name, nil)
2735
end
2836

2937
@spec all_flags() :: {:ok, [FunWithFlags.Flag.t]} | {:error, any()}
3038
def all_flags do
3139
persistence_adapter().all_flags()
40+
|> Telemetry.emit_persistence_event(:read_all_flags, nil, nil)
3241
end
3342

3443
@spec all_flag_names() :: {:ok, [atom]} | {:error, any()}
3544
def all_flag_names do
3645
persistence_adapter().all_flag_names()
46+
|> Telemetry.emit_persistence_event(:read_all_flag_names, nil, nil)
3747
end
3848
end

lib/fun_with_flags/store.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule FunWithFlags.Store do
33

44
require Logger
55
alias FunWithFlags.Store.Cache
6-
alias FunWithFlags.{Config, Flag}
6+
alias FunWithFlags.{Config, Flag, Telemetry}
77

88
import FunWithFlags.Config, only: [persistence_adapter: 0]
99

@@ -15,9 +15,11 @@ defmodule FunWithFlags.Store do
1515
{:miss, reason, stale_value_or_nil} ->
1616
case persistence_adapter().get(flag_name) do
1717
{:ok, flag} ->
18+
Telemetry.emit_persistence_event({:ok, nil}, :read, flag_name, nil)
1819
Cache.put(flag)
1920
{:ok, flag}
20-
{:error, _reason} ->
21+
err = {:error, _reason} ->
22+
Telemetry.emit_persistence_event(err, :read, flag_name, nil)
2123
try_to_use_the_cached_value(reason, stale_value_or_nil, flag_name)
2224
end
2325
end
@@ -37,6 +39,7 @@ defmodule FunWithFlags.Store do
3739
def put(flag_name, gate) do
3840
flag_name
3941
|> persistence_adapter().put(gate)
42+
|> Telemetry.emit_persistence_event(:write, flag_name, gate)
4043
|> publish_change()
4144
|> cache_persistence_result()
4245
end
@@ -46,6 +49,7 @@ defmodule FunWithFlags.Store do
4649
def delete(flag_name, gate) do
4750
flag_name
4851
|> persistence_adapter().delete(gate)
52+
|> Telemetry.emit_persistence_event(:delete_gate, flag_name, gate)
4953
|> publish_change()
5054
|> cache_persistence_result()
5155
end
@@ -55,6 +59,7 @@ defmodule FunWithFlags.Store do
5559
def delete(flag_name) do
5660
flag_name
5761
|> persistence_adapter().delete()
62+
|> Telemetry.emit_persistence_event(:delete_flag, flag_name, nil)
5863
|> publish_change()
5964
|> cache_persistence_result()
6065
end
@@ -65,19 +70,22 @@ defmodule FunWithFlags.Store do
6570
Logger.debug fn -> "FunWithFlags: reloading cached flag '#{flag_name}' from storage " end
6671
flag_name
6772
|> persistence_adapter().get()
73+
|> Telemetry.emit_persistence_event(:reload, flag_name, nil)
6874
|> cache_persistence_result()
6975
end
7076

7177

7278
@spec all_flags() :: {:ok, [FunWithFlags.Flag.t]} | {:error, any()}
7379
def all_flags do
7480
persistence_adapter().all_flags()
81+
|> Telemetry.emit_persistence_event(:read_all_flags, nil, nil)
7582
end
7683

7784

7885
@spec all_flag_names() :: {:ok, [atom]} | {:error, any()}
7986
def all_flag_names do
8087
persistence_adapter().all_flag_names()
88+
|> Telemetry.emit_persistence_event(:read_all_flag_names, nil, nil)
8189
end
8290

8391
defp cache_persistence_result(result = {:ok, flag}) do

lib/fun_with_flags/telemetry.ex

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
defmodule FunWithFlags.Telemetry do
2+
@moduledoc """
3+
Telemetry events for FunWithFlags.
4+
5+
This module centralizes the emission of all [Telemetry](https://hexdocs.pm/telemetry/readme.html)
6+
events for the package.
7+
8+
## Events
9+
10+
The common prefix for all events is `:fun_with_flags`, followed by a logical
11+
scope (e.g. `:persistence`) and the event name.
12+
13+
Events are simple "point in time" events rather than span events (that is,
14+
there is no distinct `:start` and `:stop` events with a duration measurement).
15+
16+
### Persistence
17+
18+
Events for CRUD operations on the persistent datastore.
19+
20+
All events contain the same measurement:
21+
* `system_time` (integer), which is the current system time in the
22+
`:native` time unit. See `:erlang.system_time/0`.
23+
24+
Events:
25+
26+
* `[:fun_with_flags, :persistence, :read]`, emitted when a flag is read from
27+
the DB. Crucially, this event is not emitted when the cache is enabled and
28+
there is a cache hit, and it's emitted only when retrieving a flag reads
29+
from the persistent datastore. Therefore, when the cache is disabled, this
30+
event is always emitted every time a flag is queried.
31+
32+
Metadata:
33+
* `flag_name` (string), the name of the flag being read.
34+
35+
* `[:fun_with_flags, :persistence, :read_all_flags]`, emitted when all flags
36+
are read from the DB. No extra metadata.
37+
38+
* `[:fun_with_flags, :persistence, :read_all_flag_names]`, emitted when all
39+
flags names are read from the DB. No extra metadata.
40+
41+
* `[:fun_with_flags, :persistence, :write]`, emitted when writing a flag to
42+
the DB. In practive, what is written is one of the gates of the flag, which
43+
is always upserted.
44+
45+
Metadata:
46+
* `flag_name` (string), the name of the flag being written.
47+
* `gate` (`FunWithFlags.Gate`), the gate being upserted.
48+
49+
* `[:fun_with_flags, :persistence, :delete_flag]`, emitted when an entire flag
50+
is deleted from the DB.
51+
52+
Metadata:
53+
* `flag_name` (string), the name of the flag being deleted.
54+
55+
* `[:fun_with_flags, :persistence, :delete_gate]`, emitted when one of the flag's
56+
gates is deleted from the DB.
57+
58+
Metadata:
59+
* `flag_name` (string), the name of the flag whose gate is being deleted.
60+
* `gate` (`FunWithFlags.Gate`), the gate being deleted.
61+
62+
* `[:fun_with_flags, :persistence, :reload]`, emitted when a flag is reloaded
63+
from the DB. This typically happens when the node has received a change
64+
notification for a flag, which results in the cache being invalidated and
65+
the flag being reloaded from the DB.
66+
67+
Metadata:
68+
* `flag_name` (string), the name of the flag being reloaded.
69+
70+
* `[:fun_with_flags, :persistence, :error]`, emitted for erorrs in any of the
71+
above operations.
72+
73+
Metadata:
74+
* `error` (any), the error that occurred. This is typically a string or any
75+
appropriate error term returned by the underlying persistence adapters.
76+
* `original_event` (atom), the name of the original event that failed, e.g.
77+
`:read`, `:write`, `:delete_gate`, etc.
78+
* `flag_name` (string), the name of the flag being operated on, if supported
79+
by the original event.
80+
* `gate` (`FunWithFlags.Gate`), the gate being operated on, if supported by
81+
the original event.
82+
"""
83+
84+
require Logger
85+
86+
@typedoc false
87+
@type pipelining_value :: {:ok, any()} | {:error, any()}
88+
89+
# Receive the flag name as an explicit parameter rather than pattern matching
90+
# it from the `{:ok, _}` tuple, because:
91+
#
92+
# * That tuple is only available on success, and it's therefore not available
93+
# when pipelining on an error.
94+
# * It makes it possible to use this function even when the :ok result does
95+
# not contain a flag.
96+
#
97+
@doc false
98+
@spec emit_persistence_event(
99+
pipelining_value(),
100+
event_name :: atom(),
101+
flag_name :: (atom() | nil),
102+
gate :: (FunWithFlags.Gate.t | nil)
103+
) :: pipelining_value()
104+
def emit_persistence_event(result = {:ok, _}, event_name, flag_name, gate) do
105+
metadata = %{
106+
flag_name: flag_name,
107+
gate: gate,
108+
}
109+
110+
do_send_event([:fun_with_flags, :persistence, event_name], metadata)
111+
result
112+
end
113+
114+
def emit_persistence_event(result = {:error, reason}, event_name, flag_name, gate) do
115+
metadata = %{
116+
flag_name: flag_name,
117+
gate: gate,
118+
error: reason,
119+
original_event: event_name
120+
}
121+
122+
do_send_event([:fun_with_flags, :persistence, :error], metadata)
123+
result
124+
end
125+
126+
@doc false
127+
@spec do_send_event([atom], :telemetry.event_metadata()) :: :ok
128+
def do_send_event(event_name, metadata) do
129+
measurements = %{
130+
system_time: :erlang.system_time()
131+
}
132+
133+
Logger.debug(fn ->
134+
"Telemetry event: #{inspect(event_name)}, metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}"
135+
end)
136+
137+
:telemetry.execute(event_name, measurements, metadata)
138+
end
139+
140+
141+
@doc """
142+
Attach a debug handler to FunWithFlags telemetry events.
143+
144+
Attach a Telemetry handler that logs all events at the `:alert` level.
145+
It uses the `:alert` level rather than `:debug` or `:info` simply to make it
146+
more convenient to eyeball these logs and to print them while running the tests.
147+
"""
148+
@spec attach_debug_handler() :: :ok | {:error, :already_exists}
149+
def attach_debug_handler do
150+
events = [
151+
[:fun_with_flags, :persistence, :read],
152+
[:fun_with_flags, :persistence, :read_all_flags],
153+
[:fun_with_flags, :persistence, :read_all_flag_names],
154+
[:fun_with_flags, :persistence, :write],
155+
[:fun_with_flags, :persistence, :delete_flag],
156+
[:fun_with_flags, :persistence, :delete_gate],
157+
[:fun_with_flags, :persistence, :reload],
158+
[:fun_with_flags, :persistence, :error],
159+
]
160+
161+
:telemetry.attach_many("local-debug-handler", events, &__MODULE__.debug_event_handler/4, %{})
162+
end
163+
164+
@doc false
165+
def debug_event_handler([:fun_with_flags, :persistence, event], %{system_time: system_time}, metadata, _config) do
166+
dt = DateTime.from_unix!(system_time, :native) |> DateTime.to_iso8601()
167+
168+
Logger.alert(fn ->
169+
"FunWithFlags telemetry event: #{event}, system_time: #{dt}, metadata: #{inspect(metadata)}"
170+
end)
171+
172+
:ok
173+
end
174+
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ defmodule FunWithFlags.Mixfile do
7474
{:postgrex, "~> 0.16", optional: true, only: [:dev, :test]},
7575
{:myxql, "~> 0.2", optional: true, only: [:dev, :test]},
7676
{:phoenix_pubsub, "~> 2.0", optional: true},
77+
{:telemetry, "~> 1.3"},
7778

7879
{:mock, "~> 0.3", only: :test},
7980

0 commit comments

Comments
 (0)