Skip to content

Commit ea95df1

Browse files
committed
feat(caching): add decorator context to telemetry metadata (#248)
1 parent 48ea030 commit ea95df1

File tree

7 files changed

+117
-42
lines changed

7 files changed

+117
-42
lines changed

CHANGELOG.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44

55
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [v3.0.0](https://github.com/elixir-nebulex/nebulex/tree/v3.0.0) (2026-02-17)
7+
## [v3.0.0](https://github.com/elixir-nebulex/nebulex/tree/v3.0.0) (2026-02-21)
88
> [Full Changelog](https://github.com/elixir-nebulex/nebulex/compare/v3.0.0-rc.2...v3.0.0)
99
1010
### Enhancements
@@ -21,6 +21,19 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2121
decorator level, with decorator-level settings overriding module-level
2222
defaults.
2323
[#248](https://github.com/elixir-nebulex/nebulex/issues/248).
24+
- [Nebulex.Caching.Decorators] Decorator-invoked cache operations
25+
(`cacheable`, `cache_put`, `cache_evict`) now automatically inject the
26+
`:decorator_context` into the `:telemetry_metadata` option. This enables
27+
telemetry handlers to identify which decorated function triggered each
28+
cache operation. The `:decorator_context` map includes `:decorator`,
29+
`:module`, `:function_name`, and `:arity`.
30+
[#251](https://github.com/elixir-nebulex/nebulex/issues/251).
31+
- [Nebulex.Caching.Decorators] The `cache_evict` decorator now properly
32+
passes `opts` through all eviction paths (`delete`, `delete_all`),
33+
ensuring telemetry metadata propagates consistently.
34+
- [Nebulex.Adapter] The `:telemetry_metadata` and `:telemetry_event`
35+
options are now extracted from `opts` before passing them to the adapter
36+
callback, keeping adapter args clean.
2437
- [Nebulex.Adapters.Coherent] Added new coherent cache adapter to
2538
`nebulex_distributed`. The coherent adapter provides a "local cache with
2639
distributed invalidation" pattern where each node maintains its own
@@ -129,8 +142,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
129142
`use Nebulex.Caching` (instead of adding them to every single decorated
130143
function): `:cache`, `:on_error`, `:match`, and `:opts`.
131144

132-
[q_spec]: http://hexdocs.pm/nebulex/3.0.0-rc.1/Nebulex.Cache.html#c:get_all/2-query-specification
133-
[match_ref]: http://hexdocs.pm/nebulex/3.0.0-rc.1/Nebulex.Caching.Decorators.html#cacheable/3-the-match-function-on-references
145+
[q_spec]: https://hexdocs.pm/nebulex/Nebulex.Cache.html#c:get_all/2-query-specification
146+
[match_ref]: https://hexdocs.pm/nebulex/Nebulex.Caching.Decorators.html#cacheable/3-the-match-function-on-references
134147

135148
### Backwards incompatible changes
136149

lib/nebulex/adapter.ex

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,24 @@ defmodule Nebulex.Adapter do
147147
) do
148148
opts = Options.validate_runtime_shared_opts!(opts)
149149
{telemetry?, opts} = Keyword.pop_first(opts, :telemetry, telemetry?)
150-
args = args ++ [opts]
151150

152151
if telemetry? do
152+
{telemetry_metadata, opts} = Keyword.pop_first(opts, :telemetry_metadata, %{})
153+
154+
{telemetry_event, opts} =
155+
Keyword.pop_first(opts, :telemetry_event, telemetry_prefix ++ [:command])
156+
157+
args = args ++ [opts]
158+
153159
metadata = %{
154160
adapter_meta: adapter_meta,
155161
command: command,
156162
args: args,
157-
extra_metadata: Keyword.get(opts, :telemetry_metadata, %{})
163+
extra_metadata: telemetry_metadata
158164
}
159165

160-
opts
161-
|> Keyword.get(:telemetry_event, telemetry_prefix ++ [:command])
162-
|> Telemetry.span(
166+
Telemetry.span(
167+
telemetry_event,
163168
metadata,
164169
fn ->
165170
result = apply(adapter, command, [adapter_meta | args])
@@ -168,7 +173,7 @@ defmodule Nebulex.Adapter do
168173
end
169174
)
170175
else
171-
apply(adapter, command, [adapter_meta | args])
176+
apply(adapter, command, [adapter_meta | args ++ [opts]])
172177
end
173178
end
174179

lib/nebulex/cache.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ defmodule Nebulex.Cache do
144144
first one, since the adapter's metadata is available in the event's
145145
metadata.
146146
* `:extra_metadata` - Additional metadata through the runtime option
147-
`:telemetry_metadata.`
147+
`:telemetry_metadata`.
148148
149149
Example event data:
150150
@@ -176,7 +176,7 @@ defmodule Nebulex.Cache do
176176
first one, since the adapter's metadata is available in the event's
177177
metadata.
178178
* `:extra_metadata` - Additional metadata through the runtime option
179-
`:telemetry_metadata.`
179+
`:telemetry_metadata`.
180180
* `:result` - The command's result.
181181
182182
Example event data:
@@ -211,7 +211,7 @@ defmodule Nebulex.Cache do
211211
first one, since the adapter's metadata is available in the event's
212212
metadata.
213213
* `:extra_metadata` - Additional metadata through the runtime option
214-
`:telemetry_metadata.`
214+
`:telemetry_metadata`.
215215
* `:kind` - The type of the error: `:error`, `:exit`, or `:throw`.
216216
* `:reason` - The reason of the error.
217217
* `:stacktrace` - Exception's stack trace.

lib/nebulex/caching/decorators.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,14 +1784,14 @@ if Code.ensure_loaded?(Decorator.Define) do
17841784
opts = unquote(opts_var)
17851785

17861786
# Push the decorator context onto the stack
1787-
Context.push(unquote(context))
1787+
_ignore = Context.push(unquote(context))
17881788

17891789
try do
17901790
# Execute the decorated function's code block
17911791
unquote(action_block)
17921792
after
17931793
# Pop the decorator context from the stack
1794-
Context.pop()
1794+
_ignore = Context.pop()
17951795
end
17961796
end
17971797
end
@@ -1917,6 +1917,7 @@ if Code.ensure_loaded?(Decorator.Define) do
19171917
Runtime.eval_cache_evict(
19181918
cache,
19191919
unquote(key),
1920+
opts,
19201921
unquote(before_invocation?),
19211922
unquote(all_entries?),
19221923
unquote(on_error),
@@ -1970,8 +1971,6 @@ if Code.ensure_loaded?(Decorator.Define) do
19701971
end
19711972
end
19721973

1973-
## Private functions
1974-
19751974
defp on_error_opt(attrs, default) do
19761975
get_option(attrs, :on_error, ":raise or :nothing", &(&1 in [:raise, :nothing]), default)
19771976
end

lib/nebulex/caching/decorators/runtime.ex

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,20 @@ defmodule Nebulex.Caching.Decorators.Runtime do
2121
context = Context.get()
2222
cache = eval_cache(cache, context)
2323
key = eval_key(key, context)
24+
opts = add_telemetry_metadata(opts, context)
2425

2526
do_eval_cacheable(cache, key, references, opts, match, on_error, block_fun)
2627
end
2728

2829
@doc false
29-
@spec eval_cache_evict(any(), any(), boolean(), boolean(), atom(), fun()) :: any()
30-
def eval_cache_evict(cache, key, before?, all_entries?, on_error, block_fun) do
30+
@spec eval_cache_evict(any(), any(), keyword(), boolean(), boolean(), atom(), fun()) :: any()
31+
def eval_cache_evict(cache, key, opts, before?, all_entries?, on_error, block_fun) do
3132
context = Context.get()
3233
cache = eval_cache(cache, context)
3334
key = eval_key(key, context)
35+
opts = add_telemetry_metadata(opts, context)
3436

35-
do_eval_cache_evict(cache, key, before?, all_entries?, on_error, block_fun)
37+
do_eval_cache_evict(cache, key, opts, before?, all_entries?, on_error, block_fun)
3638
end
3739

3840
@doc false
@@ -41,6 +43,7 @@ defmodule Nebulex.Caching.Decorators.Runtime do
4143
context = Context.get()
4244
cache = eval_cache(cache, context)
4345
key = eval_key(key, context)
46+
opts = add_telemetry_metadata(opts, context)
4447

4548
do_eval_cache_put(cache, key, value, opts, on_error, match)
4649
end
@@ -145,7 +148,7 @@ defmodule Nebulex.Caching.Decorators.Runtime do
145148
fn value ->
146149
with false <- do_eval_cache_put(ref_cache, ref_key, value, opts, on_error, match) do
147150
# The match returned `false`, remove the parent's key reference
148-
_ignore = do_apply(cache, :delete, [key])
151+
_ignore = do_apply(cache, :delete, [key, Keyword.take(opts, [:telemetry_metadata])])
149152

150153
false
151154
end
@@ -154,7 +157,7 @@ defmodule Nebulex.Caching.Decorators.Runtime do
154157
case eval_function(match, value) do
155158
false ->
156159
# Remove the parent's key reference
157-
_ignore = do_apply(cache, :delete, [key])
160+
_ignore = do_apply(cache, :delete, [key, Keyword.take(opts, [:telemetry_metadata])])
158161

159162
block_fun.()
160163

@@ -227,48 +230,48 @@ defmodule Nebulex.Caching.Decorators.Runtime do
227230
raise reason
228231
end
229232

230-
defp do_eval_cache_evict(cache, key, true, all_entries?, on_error, block_fun) do
231-
_ignore = do_evict(all_entries?, cache, key, on_error)
233+
defp do_eval_cache_evict(cache, key, opts, true, all_entries?, on_error, block_fun) do
234+
_ignore = do_evict(all_entries?, cache, key, opts, on_error)
232235

233236
block_fun.()
234237
end
235238

236-
defp do_eval_cache_evict(cache, key, false, all_entries?, on_error, block_fun) do
239+
defp do_eval_cache_evict(cache, key, opts, false, all_entries?, on_error, block_fun) do
237240
result = block_fun.()
238241

239-
_ignore = do_evict(all_entries?, cache, key, on_error)
242+
_ignore = do_evict(all_entries?, cache, key, opts, on_error)
240243

241244
result
242245
end
243246

244-
defp do_evict(false, cache, {:in, keys}, on_error) do
247+
defp do_evict(false, cache, {:in, keys}, opts, on_error) do
245248
keys
246249
|> group_by_cache(cache)
247250
|> Enum.each(fn
248251
{cache, [key]} ->
249-
run_cmd(cache, :delete, [key, []], on_error)
252+
run_cmd(cache, :delete, [key, opts], on_error)
250253

251254
{cache, keys} ->
252-
run_cmd(cache, :delete_all, [[in: keys]], on_error)
255+
run_cmd(cache, :delete_all, [[in: keys], opts], on_error)
253256
end)
254257
end
255258

256-
defp do_evict(false, cache, {:query, _} = q, on_error) do
257-
run_cmd(cache, :delete_all, [[q]], on_error)
259+
defp do_evict(false, cache, {:query, _} = q, opts, on_error) do
260+
run_cmd(cache, :delete_all, [[q], opts], on_error)
258261
end
259262

260-
defp do_evict(false, cache, {:query, q, k}, on_error) do
261-
_ignore = run_cmd(cache, :delete_all, [[{:query, q}]], on_error)
263+
defp do_evict(false, cache, {:query, q, k}, opts, on_error) do
264+
_ignore = run_cmd(cache, :delete_all, [[query: q], opts], on_error)
262265

263-
do_evict(false, cache, k, on_error)
266+
do_evict(false, cache, k, opts, on_error)
264267
end
265268

266-
defp do_evict(false, cache, key, on_error) do
267-
run_cmd(cache, :delete, [key, []], on_error)
269+
defp do_evict(false, cache, key, opts, on_error) do
270+
run_cmd(cache, :delete, [key, opts], on_error)
268271
end
269272

270-
defp do_evict(true, cache, _key, on_error) do
271-
run_cmd(cache, :delete_all, [], on_error)
273+
defp do_evict(true, cache, _key, opts, on_error) do
274+
run_cmd(cache, :delete_all, [[query: nil], opts], on_error)
272275
end
273276

274277
defp do_eval_cache_put(
@@ -354,6 +357,25 @@ defmodule Nebulex.Caching.Decorators.Runtime do
354357
)
355358
end
356359

360+
defp add_telemetry_metadata(opts, %Context{
361+
decorator: decorator,
362+
module: m,
363+
function_name: f,
364+
arity: a
365+
}) do
366+
ctx = %{
367+
decorator: decorator,
368+
module: m,
369+
function_name: f,
370+
arity: a
371+
}
372+
373+
Keyword.update(opts, :telemetry_metadata, %{decorator_context: ctx}, fn
374+
meta when is_map(meta) -> Map.put(meta, :decorator_context, ctx)
375+
meta -> meta
376+
end)
377+
end
378+
357379
@compile inline: [raise_invalid_cache: 1]
358380
@spec raise_invalid_cache(any()) :: no_return()
359381
defp raise_invalid_cache(cache) do

test/nebulex/caching_test.exs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ defmodule Nebulex.CachingTest do
245245
refute Cache.get!(1)
246246
end
247247

248+
test "with invalid opts" do
249+
assert_raise NimbleOptions.ValidationError,
250+
~r"invalid value for :telemetry_metadata option: expected map, got: :invalid",
251+
fn ->
252+
get_with_invalid_opts(1)
253+
end
254+
end
255+
248256
test "with match function" do
249257
refute Cache.get!(:x)
250258
assert get_with_match(:x) == :x
@@ -751,6 +759,13 @@ defmodule Nebulex.CachingTest do
751759
refute Cache.get!(:b)
752760
refute Cache.get!(:e)
753761
end
762+
763+
test "with opts" do
764+
assert set_keys(x: 1, y: 2, z: 3) == :ok
765+
766+
assert evict_with_opts(:x) == :x
767+
refute Cache.get!(:x)
768+
end
754769
end
755770

756771
describe "option :key with a custom key generator in annotation" do
@@ -1063,11 +1078,19 @@ defmodule Nebulex.CachingTest do
10631078
{x, y * 2}
10641079
end
10651080

1066-
@decorate cacheable(cache: &__MODULE__.target_cache/0, opts: [ttl: 1000])
1081+
@decorate cacheable(
1082+
cache: &__MODULE__.target_cache/0,
1083+
opts: [ttl: 1000, telemetry_metadata: %{foo: :bar}]
1084+
)
10671085
def get_with_opts(x) do
10681086
x
10691087
end
10701088

1089+
@decorate cacheable(opts: [ttl: 1000, telemetry_metadata: :invalid])
1090+
def get_with_invalid_opts(x) do
1091+
x
1092+
end
1093+
10711094
@decorate cacheable()
10721095
def get_false_with_side_effect(v) do
10731096
_ = Cache.update!("side-effect", 1, &(&1 + 1))
@@ -1127,7 +1150,11 @@ defmodule Nebulex.CachingTest do
11271150
end
11281151
end
11291152

1130-
@decorate cache_put(cache: dynamic_cache(Cache, Cache), key: {:in, [x]}, opts: [ttl: 1000])
1153+
@decorate cache_put(
1154+
cache: dynamic_cache(Cache, Cache),
1155+
key: {:in, [x]},
1156+
opts: [ttl: 1000, telemetry_metadata: %{foo: :bar}]
1157+
)
11311158
def update_with_opts(x) do
11321159
x
11331160
end
@@ -1164,6 +1191,11 @@ defmodule Nebulex.CachingTest do
11641191
x
11651192
end
11661193

1194+
@decorate cache_evict(key: x, opts: [telemetry_metadata: %{foo: :bar}])
1195+
def evict_with_opts(x) do
1196+
x
1197+
end
1198+
11671199
@decorate cacheable(key: x)
11681200
def multiple_clauses(x, y \\ 0)
11691201

0 commit comments

Comments
 (0)