Packages

appsignal

2.17.2
2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.0 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.1 2.14.0 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.3 2.12.2 2.12.1 2.12.0 2.11.0 2.10.2 2.10.1 2.10.0 2.9.2 2.9.1 2.9.0 2.8.3 2.8.2 2.8.1 2.8.0 2.7.13 2.7.12 2.7.11 2.7.10 2.7.9 2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 2.7.3 2.7.2 2.7.1 2.7.0 2.7.0-beta.1 2.6.1 2.6.0 2.5.3 2.5.2 2.5.1 2.5.0 2.4.3 2.4.2 2.4.1 2.4.0 2.3.1 2.3.0 2.2.19 2.2.18 2.2.17 2.2.16 2.2.15 2.2.14 2.2.13 2.2.12 retired 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.7 2.1.7-beta.1 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.8 2.0.7 2.0.6 2.0.6-beta.1 2.0.5 retired 2.0.4 retired 2.0.3 retired 2.0.2 retired 2.0.1 retired 2.0.0 retired 2.0.0-beta.11 2.0.0-beta.10 2.0.0-beta.9 2.0.0-beta.8 2.0.0-beta.7 2.0.0-beta.6 2.0.0-beta.5 2.0.0-beta.4 2.0.0-beta.3 2.0.0-beta.2 2.0.0-beta.1 2.0.0-alpha.7 2.0.0-alpha.6 2.0.0-alpha.5 2.0.0-alpha.4 2.0.0-alpha.3 2.0.0-alpha.2 2.0.0-alpha.1 1.13.5 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.13.0-beta.2 1.13.0-beta.1 1.12.1 1.12.0 1.12.0-beta.3 1.12.0-beta.2 1.12.0-beta.1 1.11.8 1.11.7 1.11.6 1.11.5 1.11.4 1.11.3 1.11.2 1.11.1 1.11.0 1.11.0-beta.1 1.10.13 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.4 1.9.3 1.9.2 1.9.1 1.9.1-beta.1 1.9.0 1.9.0-beta.1 1.9.0-alpha.1 1.8.2 1.8.1 1.8.1-beta.2 1.8.1-beta.1 1.8.0 1.7.2 1.7.1 1.7.0 1.7.0-beta.2 1.7.0-beta.1 1.7.0-alpha.4 1.7.0-alpha.3 1.7.0-alpha.2 1.7.0-alpha.1 1.6.7 1.6.6 1.6.6-beta.2 1.6.6-beta.1 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.6.0-beta.1 1.6.0-alpha.1 1.5.1 1.5.0 1.5.0-beta.2 1.5.0-beta.1 1.4.11-beta.1 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.4.0-alpha.1 1.3.6 1.3.6-beta.1 1.3.5 1.3.4 1.3.4-beta.1 1.3.3 1.3.2 1.3.1 1.3.0 1.3.0-beta.3 1.3.0-beta.2 1.3.0-beta.1 1.2.3 1.2.2 1.2.1 1.2.0 1.1.1 1.1.0 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 1.0.0-rc.1 0.13.0 0.12.3 0.12.2 0.12.1 0.12.0 0.11.6 0.11.5 0.11.4 0.11.3 0.11.2 0.11.1 0.11.0 0.10.0 0.9.2 0.9.1 0.9.0 0.8.0 0.7.1 0.7.0 0.6.2 0.6.1 0.6.0 0.5.0 0.4.0 0.3.0 0.2.0 0.1.0 0.0.9 0.0.8 0.0.7 0.0.6 0.0.5 0.0.4 0.0.3 0.0.2 0.0.1

Collects error and performance data from your Elixir applications and sends it to AppSignal

Current section

280 Versions

Jump to

Compare versions

6 files changed
+138 additions
-53 deletions
  @@ -1,5 +1,29 @@
1 1 # AppSignal for Elixir changelog
2 2
3 + ## 2.17.2
4 +
5 + _Published on 2026-05-26._
6 +
7 + ### Added
8 +
9 + - Emit distribution metrics from the Ecto integration.
10 +
11 + Each Ecto query telemetry event now also reports its timing values as `ecto_query_time`, `ecto_queue_time`, `ecto_decode_time`, `ecto_idle_time` and `ecto_total_time` distribution metrics.
12 +
13 + All five metrics are tagged by `repo` and `hostname`. The `ecto_query_time`, `ecto_decode_time` and `ecto_total_time` metrics are additionally tagged by `repo` and `source` (the Ecto table).
14 +
15 + This surfaces pool waits, decode time, idle connection time and per-table latency as standalone metrics in addition to the existing query span.
16 +
17 + (patch [7440fb76](https://github.com/appsignal/appsignal-elixir/commit/7440fb769af74fca362dd5a2b04f9c5bfe420583))
18 +
19 + ### Fixed
20 +
21 + - Preserve sub-millisecond precision when reporting timing distribution metrics.
22 +
23 + The Ecto (`ecto_query_time`, `ecto_queue_time`, `ecto_decode_time`, `ecto_idle_time`, `ecto_total_time`) and Oban (`oban_job_duration`, `oban_job_queue_time`) distribution metrics now report fractional milliseconds, instead of being truncated to whole milliseconds. Sub-millisecond measurements were previously rounded down to zero.
24 +
25 + (patch [262fef55](https://github.com/appsignal/appsignal-elixir/commit/262fef550c5df4897fb70a2a49e6ac3eb4d155c2))
26 +
3 27 ## 2.17.1
4 28
5 29 _Published on 2026-05-14._
  @@ -3,57 +3,10 @@
3 3 <<"https://github.com/appsignal/appsignal-elixir/blob/main/CHANGELOG.md">>},
4 4 {<<"GitHub">>,<<"https://github.com/appsignal/appsignal-elixir">>}]}.
5 5 {<<"name">>,<<"appsignal">>}.
6 - {<<"version">>,<<"2.17.1">>}.
6 + {<<"version">>,<<"2.17.2">>}.
7 7 {<<"description">>,
8 8 <<"Collects error and performance data from your Elixir applications and sends it to AppSignal">>}.
9 9 {<<"elixir">>,<<"~> 1.9">>}.
10 - {<<"app">>,<<"appsignal">>}.
11 - {<<"requirements">>,
12 - [[{<<"name">>,<<"castore">>},
13 - {<<"app">>,<<"castore">>},
14 - {<<"optional">>,false},
15 - {<<"requirement">>,<<"~> 1.0">>},
16 - {<<"repository">>,<<"hexpm">>}],
17 - [{<<"name">>,<<"certifi">>},
18 - {<<"app">>,<<"certifi">>},
19 - {<<"optional">>,false},
20 - {<<"requirement">>,<<"~> 2.14">>},
21 - {<<"repository">>,<<"hexpm">>}],
22 - [{<<"name">>,<<"decimal">>},
23 - {<<"app">>,<<"decimal">>},
24 - {<<"optional">>,false},
25 - {<<"requirement">>,<<"~> 2.0 or ~> 3.1">>},
26 - {<<"repository">>,<<"hexpm">>}],
27 - [{<<"name">>,<<"finch">>},
28 - {<<"app">>,<<"finch">>},
29 - {<<"optional">>,false},
30 - {<<"requirement">>,<<"~> 0.19">>},
31 - {<<"repository">>,<<"hexpm">>}],
32 - [{<<"name">>,<<"jason">>},
33 - {<<"app">>,<<"jason">>},
34 - {<<"optional">>,false},
35 - {<<"requirement">>,<<"~> 1.0">>},
36 - {<<"repository">>,<<"hexpm">>}],
37 - [{<<"name">>,<<"decorator">>},
38 - {<<"app">>,<<"decorator">>},
39 - {<<"optional">>,false},
40 - {<<"requirement">>,<<"~> 1.2.3 or ~> 1.3">>},
41 - {<<"repository">>,<<"hexpm">>}],
42 - [{<<"name">>,<<"telemetry">>},
43 - {<<"app">>,<<"telemetry">>},
44 - {<<"optional">>,false},
45 - {<<"requirement">>,<<"~> 0.4 or ~> 1.0">>},
46 - {<<"repository">>,<<"hexpm">>}],
47 - [{<<"name">>,<<"httpoison">>},
48 - {<<"app">>,<<"httpoison">>},
49 - {<<"optional">>,true},
50 - {<<"requirement">>,<<"~> 2.0">>},
51 - {<<"repository">>,<<"hexpm">>}],
52 - [{<<"name">>,<<"logger_backends">>},
53 - {<<"app">>,<<"logger_backends">>},
54 - {<<"optional">>,false},
55 - {<<"requirement">>,<<"~> 1.0">>},
56 - {<<"repository">>,<<"hexpm">>}]]}.
57 10 {<<"files">>,
58 11 [<<"lib">>,<<"lib/appsignal.ex">>,<<"lib/appsignal">>,
59 12 <<"lib/appsignal/transaction.ex">>,
  @@ -105,5 +58,52 @@
105 58 <<"mix.exs">>,<<"mix_helpers.exs">>,<<"CHANGELOG.md">>,
106 59 <<"CODE_OF_CONDUCT.md">>,<<"README.md">>,<<"SUPPORT.md">>,<<"LICENSE">>,
107 60 <<"Makefile">>,<<"agent.exs">>,<<"priv/cacert.pem">>]}.
61 + {<<"app">>,<<"appsignal">>}.
62 + {<<"requirements">>,
63 + [[{<<"name">>,<<"castore">>},
64 + {<<"app">>,<<"castore">>},
65 + {<<"optional">>,false},
66 + {<<"requirement">>,<<"~> 1.0">>},
67 + {<<"repository">>,<<"hexpm">>}],
68 + [{<<"name">>,<<"certifi">>},
69 + {<<"app">>,<<"certifi">>},
70 + {<<"optional">>,false},
71 + {<<"requirement">>,<<"~> 2.14">>},
72 + {<<"repository">>,<<"hexpm">>}],
73 + [{<<"name">>,<<"decimal">>},
74 + {<<"app">>,<<"decimal">>},
75 + {<<"optional">>,false},
76 + {<<"requirement">>,<<"~> 2.0 or ~> 3.1">>},
77 + {<<"repository">>,<<"hexpm">>}],
78 + [{<<"name">>,<<"finch">>},
79 + {<<"app">>,<<"finch">>},
80 + {<<"optional">>,false},
81 + {<<"requirement">>,<<"~> 0.19">>},
82 + {<<"repository">>,<<"hexpm">>}],
83 + [{<<"name">>,<<"jason">>},
84 + {<<"app">>,<<"jason">>},
85 + {<<"optional">>,false},
86 + {<<"requirement">>,<<"~> 1.0">>},
87 + {<<"repository">>,<<"hexpm">>}],
88 + [{<<"name">>,<<"decorator">>},
89 + {<<"app">>,<<"decorator">>},
90 + {<<"optional">>,false},
91 + {<<"requirement">>,<<"~> 1.2.3 or ~> 1.3">>},
92 + {<<"repository">>,<<"hexpm">>}],
93 + [{<<"name">>,<<"telemetry">>},
94 + {<<"app">>,<<"telemetry">>},
95 + {<<"optional">>,false},
96 + {<<"requirement">>,<<"~> 0.4 or ~> 1.0">>},
97 + {<<"repository">>,<<"hexpm">>}],
98 + [{<<"name">>,<<"httpoison">>},
99 + {<<"app">>,<<"httpoison">>},
100 + {<<"optional">>,true},
101 + {<<"requirement">>,<<"~> 2.0">>},
102 + {<<"repository">>,<<"hexpm">>}],
103 + [{<<"name">>,<<"logger_backends">>},
104 + {<<"app">>,<<"logger_backends">>},
105 + {<<"optional">>,false},
106 + {<<"requirement">>,<<"~> 1.0">>},
107 + {<<"repository">>,<<"hexpm">>}]]}.
108 108 {<<"licenses">>,[<<"MIT">>]}.
109 109 {<<"build_tools">>,[<<"mix">>,<<"make">>]}.
  @@ -3,7 +3,20 @@ defmodule Appsignal.Ecto do
3 3
4 4 @tracer Application.compile_env(:appsignal, :appsignal_tracer, Appsignal.Tracer)
5 5 @span Application.compile_env(:appsignal, :appsignal_span, Appsignal.Span)
6 - import Appsignal.Utils, only: [module_name: 1]
6 + @appsignal Application.compile_env(:appsignal, :appsignal, Appsignal)
7 + import Appsignal.Utils, only: [module_name: 1, native_to_milliseconds: 1]
8 +
9 + # For each measurement Ecto emits, the tag combinations we want to fan out to.
10 + # `:hostname` -> tagged by repo + hostname (BEAM-side dimension).
11 + # `:source` -> tagged by repo + source (table-side dimension), only relevant
12 + # for measurements that vary with the query shape.
13 + @measurement_tag_modes [
14 + query_time: [:hostname, :source],
15 + queue_time: [:hostname],
16 + decode_time: [:hostname, :source],
17 + idle_time: [:hostname],
18 + total_time: [:hostname, :source]
19 + ]
7 20
8 21 @doc """
9 22 Attaches `Appsignal.Ecto` to the Ecto telemetry channel configured in the
  @@ -63,15 +76,45 @@ defmodule Appsignal.Ecto do
63 76 @doc false
64 77 def handle_event(
65 78 _event,
66 - %{total_time: total_time},
79 + %{total_time: total_time} = measurements,
67 80 %{repo: repo, query: query} = metadata,
68 81 _config
69 82 ) do
83 + add_measurement_distributions(repo, measurements, metadata)
70 84 do_handle_event(current_span(metadata), total_time, repo, query)
71 85 end
72 86
73 87 def handle_event(_event, _measurements, _metadata, _config), do: :ok
74 88
89 + defp add_measurement_distributions(repo, measurements, metadata) do
90 + repo_name = module_name(repo)
91 + hostname_tags = %{repo: repo_name, hostname: Appsignal.Utils.Hostname.hostname()}
92 + source_tags = source_tags(repo_name, metadata)
93 +
94 + Enum.each(@measurement_tag_modes, fn {key, modes} ->
95 + value = Map.get(measurements, key)
96 +
97 + if is_integer(value) do
98 + ms = native_to_milliseconds(value)
99 + metric = "ecto_#{key}"
100 +
101 + if :hostname in modes do
102 + @appsignal.add_distribution_value(metric, ms, hostname_tags)
103 + end
104 +
105 + if :source in modes and source_tags do
106 + @appsignal.add_distribution_value(metric, ms, source_tags)
107 + end
108 + end
109 + end)
110 + end
111 +
112 + defp source_tags(repo_name, %{source: source}) when is_binary(source) do
113 + %{repo: repo_name, source: source}
114 + end
115 +
116 + defp source_tags(_repo_name, _metadata), do: nil
117 +
75 118 defp current_span(metadata) do
76 119 # If a current span is already set in this process, use that instead
77 120 # of the current span passed as part of the Ecto metadata.
  @@ -5,6 +5,8 @@ defmodule Appsignal.Oban do
5 5 @span Application.compile_env(:appsignal, :appsignal_span, Appsignal.Span)
6 6 @appsignal Application.compile_env(:appsignal, :appsignal, Appsignal)
7 7
8 + import Appsignal.Utils, only: [native_to_milliseconds: 1]
9 +
8 10 @moduledoc false
9 11
10 12 def attach do
  @@ -232,14 +234,14 @@ defmodule Appsignal.Oban do
232 234 Enum.each(tag_combinations, fn tags ->
233 235 @appsignal.add_distribution_value(
234 236 "oban_job_duration",
235 - System.convert_time_unit(duration, :native, :millisecond),
237 + native_to_milliseconds(duration),
236 238 tags
237 239 )
238 240 end)
239 241 end
240 242
241 243 defp add_job_queue_time_value(job, queue) do
242 - delay = DateTime.diff(job.attempted_at, job.scheduled_at, :millisecond)
244 + delay = DateTime.diff(job.attempted_at, job.scheduled_at, :microsecond) / 1000
243 245
244 246 @appsignal.add_distribution_value("oban_job_queue_time", delay, %{
245 247 queue: to_string(queue)
  @@ -19,6 +19,22 @@ defmodule Appsignal.Utils do
19 19
20 20 def module_name(module), do: module |> to_string() |> module_name()
21 21
22 + @doc """
23 + Converts a native-unit duration to fractional milliseconds, preserving
24 + sub-millisecond precision. Use this when reporting timings as distribution
25 + metric values.
26 +
27 + ## Examples
28 +
29 + iex> Appsignal.Utils.native_to_milliseconds(System.convert_time_unit(1500, :microsecond, :native))
30 + 1.5
31 +
32 + """
33 + @spec native_to_milliseconds(integer()) :: float()
34 + def native_to_milliseconds(native) when is_integer(native) do
35 + System.convert_time_unit(native, :native, :microsecond) / 1000
36 + end
37 +
22 38 def info(message) do
23 39 require Logger
24 40 Logger.info(message)
Loading more files…