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
Current section
280 Versions
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…