diff --git a/.gitignore b/.gitignore index 1f64a5e2..39814143 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ erl_crash.dump /priv/sentry.map test_integrations/phoenix_app/db + +test_integrations/*/_build +test_integrations/*/deps diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7f9724..1bdc2de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +#### Various improvements + +- Allow any version of opentelemetry deps and verify minimum versions internally - this makes it possible to use `sentry` *with tracing disabled* along with older versions of opentelemetry deps ([#931](https://github.com/getsentry/sentry-elixir/pull/931)) + ## 11.0.2 ### Bug fixes diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 8987be4e..9a41069e 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -554,6 +554,7 @@ defmodule Sentry.Config do |> normalize_included_environments() |> normalize_environment() |> handle_deprecated_before_send() + |> warn_traces_sample_rate_without_dependencies() {:error, error} -> raise ArgumentError, """ @@ -701,7 +702,10 @@ defmodule Sentry.Config do def integrations, do: fetch!(:integrations) @spec tracing?() :: boolean() - def tracing?, do: not is_nil(fetch!(:traces_sample_rate)) or not is_nil(get(:traces_sampler)) + def tracing? do + (Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() and + not is_nil(fetch!(:traces_sample_rate))) or not is_nil(get(:traces_sampler)) + end @spec put_config(atom(), term()) :: :ok def put_config(key, value) when is_atom(key) do @@ -762,6 +766,25 @@ defmodule Sentry.Config do end end + defp warn_traces_sample_rate_without_dependencies(opts) do + traces_sample_rate = Keyword.get(opts, :traces_sample_rate) + + if not is_nil(traces_sample_rate) and + not Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do + require Logger + + Logger.warning(""" + Sentry tracing is configured with traces_sample_rate: #{inspect(traces_sample_rate)}, \ + but the required OpenTelemetry dependencies are not satisfied. \ + Tracing will be disabled. Please ensure you have compatible versions of: \ + opentelemetry (>= 1.5.0), opentelemetry_api (>= 1.4.0), \ + opentelemetry_exporter (>= 1.0.0), and opentelemetry_semantic_conventions (>= 1.27.0). + """) + end + + opts + end + defp normalize_environment(config) do Keyword.update!(config, :environment_name, &to_string/1) end diff --git a/lib/sentry/opentelemetry/sampler.ex b/lib/sentry/opentelemetry/sampler.ex index c5ad6cef..17c45ade 100644 --- a/lib/sentry/opentelemetry/sampler.ex +++ b/lib/sentry/opentelemetry/sampler.ex @@ -1,4 +1,4 @@ -if Code.ensure_loaded?(:otel_sampler) do +if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do defmodule Sentry.OpenTelemetry.Sampler do @moduledoc false diff --git a/lib/sentry/opentelemetry/span_processor.ex b/lib/sentry/opentelemetry/span_processor.ex index 11bca198..c0f81203 100644 --- a/lib/sentry/opentelemetry/span_processor.ex +++ b/lib/sentry/opentelemetry/span_processor.ex @@ -1,4 +1,4 @@ -if Code.ensure_loaded?(OpenTelemetry) do +if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do defmodule Sentry.OpenTelemetry.SpanProcessor do @moduledoc false diff --git a/lib/sentry/opentelemetry/span_record.ex b/lib/sentry/opentelemetry/span_record.ex index 5d02c556..cd2a25d2 100644 --- a/lib/sentry/opentelemetry/span_record.ex +++ b/lib/sentry/opentelemetry/span_record.ex @@ -1,4 +1,4 @@ -if Code.ensure_loaded?(OpenTelemetry) do +if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do defmodule Sentry.OpenTelemetry.SpanRecord do @moduledoc false diff --git a/lib/sentry/opentelemetry/span_storage.ex b/lib/sentry/opentelemetry/span_storage.ex index d2c8ca2a..4b69b936 100644 --- a/lib/sentry/opentelemetry/span_storage.ex +++ b/lib/sentry/opentelemetry/span_storage.ex @@ -1,151 +1,153 @@ -defmodule Sentry.OpenTelemetry.SpanStorage do - @moduledoc false - use GenServer +if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do + defmodule Sentry.OpenTelemetry.SpanStorage do + @moduledoc false + use GenServer - defstruct [:cleanup_interval, :table_name] + defstruct [:cleanup_interval, :table_name] - alias Sentry.OpenTelemetry.SpanRecord + alias Sentry.OpenTelemetry.SpanRecord - @cleanup_interval :timer.minutes(5) + @cleanup_interval :timer.minutes(5) - @span_ttl 30 * 60 + @span_ttl 30 * 60 - @spec start_link(keyword()) :: GenServer.on_start() - def start_link(opts) when is_list(opts) do - name = Keyword.get(opts, :name, __MODULE__) - GenServer.start_link(__MODULE__, opts, name: name) - end + @spec start_link(keyword()) :: GenServer.on_start() + def start_link(opts) when is_list(opts) do + name = Keyword.get(opts, :name, __MODULE__) + GenServer.start_link(__MODULE__, opts, name: name) + end - @impl true - def init(opts) do - table_name = Keyword.get(opts, :table_name, default_table_name()) - cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval) + @impl true + def init(opts) do + table_name = Keyword.get(opts, :table_name, default_table_name()) + cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval) - _ = :ets.new(table_name, [:named_table, :public, :ordered_set]) + _ = :ets.new(table_name, [:named_table, :public, :ordered_set]) - schedule_cleanup(cleanup_interval) + schedule_cleanup(cleanup_interval) - {:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}} - end + {:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}} + end - @impl true - def handle_info(:cleanup_stale_spans, state) do - cleanup_stale_spans(state.table_name) - schedule_cleanup(state.cleanup_interval) + @impl true + def handle_info(:cleanup_stale_spans, state) do + cleanup_stale_spans(state.table_name) + schedule_cleanup(state.cleanup_interval) - {:noreply, state} - end + {:noreply, state} + end - @spec store_span(SpanRecord.t(), keyword()) :: true - def store_span(span_data, opts \\ []) do - table_name = Keyword.get(opts, :table_name, default_table_name()) - stored_at = System.system_time(:second) + @spec store_span(SpanRecord.t(), keyword()) :: true + def store_span(span_data, opts \\ []) do + table_name = Keyword.get(opts, :table_name, default_table_name()) + stored_at = System.system_time(:second) - if span_data.parent_span_id == nil do - :ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at}) - else - key = {:child_span, span_data.parent_span_id, span_data.span_id} + if span_data.parent_span_id == nil do + :ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at}) + else + key = {:child_span, span_data.parent_span_id, span_data.span_id} - :ets.insert(table_name, {key, span_data, stored_at}) + :ets.insert(table_name, {key, span_data, stored_at}) + end end - end - @spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil - def get_root_span(span_id, opts \\ []) do - table_name = Keyword.get(opts, :table_name, default_table_name()) + @spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil + def get_root_span(span_id, opts \\ []) do + table_name = Keyword.get(opts, :table_name, default_table_name()) - case :ets.lookup(table_name, {:root_span, span_id}) do - [{{:root_span, ^span_id}, span, _stored_at}] -> span - [] -> nil + case :ets.lookup(table_name, {:root_span, span_id}) do + [{{:root_span, ^span_id}, span, _stored_at}] -> span + [] -> nil + end end - end - @spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()] - def get_child_spans(parent_span_id, opts \\ []) do - table_name = Keyword.get(opts, :table_name, default_table_name()) + @spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()] + def get_child_spans(parent_span_id, opts \\ []) do + table_name = Keyword.get(opts, :table_name, default_table_name()) - get_all_descendants(parent_span_id, table_name) - end + get_all_descendants(parent_span_id, table_name) + end - defp get_all_descendants(parent_span_id, table_name) do - direct_children = - :ets.match_object(table_name, {{:child_span, parent_span_id, :_}, :_, :_}) - |> Enum.map(fn {_key, span_data, _stored_at} -> span_data end) + defp get_all_descendants(parent_span_id, table_name) do + direct_children = + :ets.match_object(table_name, {{:child_span, parent_span_id, :_}, :_, :_}) + |> Enum.map(fn {_key, span_data, _stored_at} -> span_data end) - nested_descendants = - Enum.flat_map(direct_children, fn child -> - get_all_descendants(child.span_id, table_name) - end) + nested_descendants = + Enum.flat_map(direct_children, fn child -> + get_all_descendants(child.span_id, table_name) + end) - (direct_children ++ nested_descendants) - |> Enum.sort_by(& &1.start_time) - end + (direct_children ++ nested_descendants) + |> Enum.sort_by(& &1.start_time) + end - @spec update_span(SpanRecord.t(), keyword()) :: :ok - def update_span(%{parent_span_id: parent_span_id} = span_data, opts \\ []) do - table_name = Keyword.get(opts, :table_name, default_table_name()) - stored_at = System.system_time(:second) + @spec update_span(SpanRecord.t(), keyword()) :: :ok + def update_span(%{parent_span_id: parent_span_id} = span_data, opts \\ []) do + table_name = Keyword.get(opts, :table_name, default_table_name()) + stored_at = System.system_time(:second) - key = - if parent_span_id == nil do - {:root_span, span_data.span_id} - else - {:child_span, parent_span_id, span_data.span_id} - end + key = + if parent_span_id == nil do + {:root_span, span_data.span_id} + else + {:child_span, parent_span_id, span_data.span_id} + end - :ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}]) + :ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}]) - :ok - end + :ok + end - @spec remove_root_span(String.t(), keyword()) :: :ok - def remove_root_span(span_id, opts \\ []) do - table_name = Keyword.get(opts, :table_name, default_table_name()) - key = {:root_span, span_id} + @spec remove_root_span(String.t(), keyword()) :: :ok + def remove_root_span(span_id, opts \\ []) do + table_name = Keyword.get(opts, :table_name, default_table_name()) + key = {:root_span, span_id} - :ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}]) - remove_child_spans(span_id, table_name: table_name) + :ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}]) + remove_child_spans(span_id, table_name: table_name) - :ok - end + :ok + end - @spec remove_child_spans(String.t(), keyword()) :: :ok - def remove_child_spans(parent_span_id, opts) do - table_name = Keyword.get(opts, :table_name, default_table_name()) + @spec remove_child_spans(String.t(), keyword()) :: :ok + def remove_child_spans(parent_span_id, opts) do + table_name = Keyword.get(opts, :table_name, default_table_name()) - :ets.select_delete(table_name, [ - {{{:child_span, parent_span_id, :_}, :_, :_}, [], [true]} - ]) + :ets.select_delete(table_name, [ + {{{:child_span, parent_span_id, :_}, :_, :_}, [], [true]} + ]) - :ok - end + :ok + end - defp schedule_cleanup(interval) do - Process.send_after(self(), :cleanup_stale_spans, interval) - end + defp schedule_cleanup(interval) do + Process.send_after(self(), :cleanup_stale_spans, interval) + end - defp cleanup_stale_spans(table_name) do - now = System.system_time(:second) - cutoff_time = now - @span_ttl + defp cleanup_stale_spans(table_name) do + now = System.system_time(:second) + cutoff_time = now - @span_ttl - root_match_spec = [ - {{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]} - ] + root_match_spec = [ + {{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]} + ] - expired_root_spans = :ets.select(table_name, root_match_spec) + expired_root_spans = :ets.select(table_name, root_match_spec) - Enum.each(expired_root_spans, fn span_id -> - remove_root_span(span_id, table_name: table_name) - end) + Enum.each(expired_root_spans, fn span_id -> + remove_root_span(span_id, table_name: table_name) + end) - child_match_spec = [ - {{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]} - ] + child_match_spec = [ + {{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]} + ] - :ets.select_delete(table_name, child_match_spec) - end + :ets.select_delete(table_name, child_match_spec) + end - defp default_table_name do - Module.concat(__MODULE__, ETSTable) + defp default_table_name do + Module.concat(__MODULE__, ETSTable) + end end end diff --git a/lib/sentry/opentelemetry/version_checker.ex b/lib/sentry/opentelemetry/version_checker.ex new file mode 100644 index 00000000..e6ae8961 --- /dev/null +++ b/lib/sentry/opentelemetry/version_checker.ex @@ -0,0 +1,73 @@ +defmodule Sentry.OpenTelemetry.VersionChecker do + @moduledoc false + + @minimum_versions %{ + opentelemetry: "1.5.0", + opentelemetry_api: "1.4.0", + opentelemetry_exporter: "1.0.0", + opentelemetry_semantic_conventions: "1.27.0" + } + + @spec tracing_compatible?() :: boolean() + def tracing_compatible? do + case check_compatibility() do + {:ok, :compatible} -> true + {:error, _} -> false + end + end + + @spec check_compatibility() :: {:ok, :compatible} | {:error, term()} + def check_compatibility do + case check_all_dependencies() do + [] -> + {:ok, :compatible} + + errors -> + {:error, {:incompatible_versions, errors}} + end + end + + defp check_all_dependencies do + @minimum_versions + |> Enum.flat_map(fn {dep, min_version} -> + case check_dependency_version(dep, min_version) do + :ok -> [] + {:error, reason} -> [{dep, reason}] + end + end) + end + + defp check_dependency_version(dep, min_version) do + case get_loaded_version(dep) do + {:ok, loaded_version} -> + if version_compatible?(loaded_version, min_version) do + :ok + else + {:error, {:version_too_old, loaded_version, min_version}} + end + + {:error, :not_loaded} -> + {:error, :not_loaded} + end + end + + defp get_loaded_version(dep) do + apps = Application.loaded_applications() + + case List.keyfind(apps, dep, 0) do + {^dep, _description, version} -> + {:ok, to_string(version)} + + nil -> + {:error, :not_loaded} + end + end + + defp version_compatible?(loaded_version, min_version) do + case Version.compare(loaded_version, min_version) do + :gt -> true + :eq -> true + :lt -> false + end + end +end diff --git a/mix.exs b/mix.exs index 7e5defd2..544ed7ba 100644 --- a/mix.exs +++ b/mix.exs @@ -118,10 +118,15 @@ defmodule Sentry.Mixfile do {:floki, ">= 0.30.0", only: :test}, {:oban, "~> 2.17 and >= 2.17.6", only: [:test]}, {:quantum, "~> 3.0", only: [:test]}, - {:opentelemetry, "~> 1.5", optional: true}, - {:opentelemetry_api, "~> 1.4", optional: true}, - {:opentelemetry_exporter, "~> 1.0", optional: true}, - {:opentelemetry_semantic_conventions, "~> 1.27", optional: true} + + # Optional dependencies for Sentry.OpenTelemetry - we allow any version + # because the actual version requirements are verified via VersionChecker. + # This is to allow users install `sentry` even when they rely on opentelemetry + # libs that are too old for Sentry tracing feature. + {:opentelemetry, ">= 0.0.0", optional: true}, + {:opentelemetry_api, ">= 0.0.0", optional: true}, + {:opentelemetry_exporter, ">= 0.0.0", optional: true}, + {:opentelemetry_semantic_conventions, ">= 0.0.0", optional: true} ] end @@ -148,6 +153,7 @@ defmodule Sentry.Mixfile do if Version.match?(System.version(), ">= 1.16.0") do run_integration_tests("umbrella", args) run_integration_tests("phoenix_app", args) + run_integration_tests("legacy_otel", args) else Mix.shell().info("Skipping integration tests for Elixir versions < 1.16") end diff --git a/test/sentry/config_traces_sampler_test.exs b/test/sentry/config_traces_sampler_test.exs index 0caf78b5..45b4b15b 100644 --- a/test/sentry/config_traces_sampler_test.exs +++ b/test/sentry/config_traces_sampler_test.exs @@ -1,5 +1,5 @@ defmodule Sentry.ConfigTracesSamplerTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false import Sentry.TestHelpers diff --git a/test/sentry/opentelemetry/integration_test.exs b/test/sentry/opentelemetry/integration_test.exs new file mode 100644 index 00000000..d3fa275a --- /dev/null +++ b/test/sentry/opentelemetry/integration_test.exs @@ -0,0 +1,105 @@ +defmodule Sentry.OpenTelemetry.IntegrationTest do + use ExUnit.Case, async: true + + alias Sentry.OpenTelemetry.VersionChecker + + describe "OpenTelemetry integration with compatible versions" do + test "modules are defined when versions are compatible" do + case VersionChecker.tracing_compatible?() do + true -> + # When versions are compatible, modules should be defined + assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) + assert Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) + assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanStorage) + + false -> + # When versions are incompatible, modules should not be defined + refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) + refute Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) + refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + # SpanStorage should always be defined as it doesn't depend on OpenTelemetry directly + assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanStorage) + end + end + + test "application startup behavior with tracing enabled" do + # This test verifies that the application startup logic works correctly + # We can't easily test the actual startup, but we can test the logic + + tracing_enabled = Sentry.Config.tracing?() + version_compatible = VersionChecker.tracing_compatible?() + + # The span storage should only be started if both conditions are true + expected_span_storage = tracing_enabled and version_compatible + + if expected_span_storage do + # If we expect span storage to be running, it should be available + assert Process.whereis(Sentry.OpenTelemetry.SpanStorage) != nil + else + # If we don't expect it, it might still be running from other tests + # so we just verify the logic is sound + assert is_boolean(tracing_enabled) + assert is_boolean(version_compatible) + end + end + + test "version checker integration with Config.tracing?" do + # Test that the version checker works correctly with Sentry's tracing configuration + tracing_configured = Sentry.Config.tracing?() + version_compatible = VersionChecker.tracing_compatible?() + + # Both should return booleans + assert is_boolean(tracing_configured) + assert is_boolean(version_compatible) + + # If tracing is not configured, version compatibility doesn't matter + # If tracing is configured, version compatibility determines if it actually works + effective_tracing = tracing_configured and version_compatible + assert is_boolean(effective_tracing) + end + end + + describe "version checker behavior" do + test "check_compatibility returns consistent results" do + # Call multiple times to ensure consistency + result1 = VersionChecker.check_compatibility() + result2 = VersionChecker.check_compatibility() + result3 = VersionChecker.tracing_compatible?() + + # Results should be consistent + assert result1 == result2 + + # tracing_compatible? should match check_compatibility result + case result1 do + {:ok, :compatible} -> assert result3 == true + {:error, _} -> assert result3 == false + end + end + end + + describe "conditional module loading" do + test "modules have correct conditional compilation" do + # Test that the conditional compilation works as expected + version_compatible = VersionChecker.tracing_compatible?() + + # Check if OpenTelemetry is available at all + otel_available = Code.ensure_loaded?(OpenTelemetry) + otel_sampler_available = Code.ensure_loaded?(:otel_sampler) + + if otel_available and version_compatible do + assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) + assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + else + refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) + refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + end + + if otel_sampler_available and version_compatible do + assert Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) + else + refute Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) + end + end + end +end diff --git a/test/sentry/opentelemetry/version_checker_test.exs b/test/sentry/opentelemetry/version_checker_test.exs new file mode 100644 index 00000000..0a399124 --- /dev/null +++ b/test/sentry/opentelemetry/version_checker_test.exs @@ -0,0 +1,48 @@ +defmodule Sentry.OpenTelemetry.VersionCheckerTest do + use ExUnit.Case, async: true + + alias Sentry.OpenTelemetry.VersionChecker + + describe "check_compatibility/0" do + test "works with current loaded dependencies" do + # This test will work with whatever OpenTelemetry versions are currently loaded + result = VersionChecker.check_compatibility() + + case result do + {:ok, :compatible} -> + assert true + + {:error, {:incompatible_versions, errors}} -> + # If we get errors, they should be properly formatted + assert is_list(errors) + assert length(errors) > 0 + + for {dep, reason} <- errors do + assert dep in [ + :opentelemetry, + :opentelemetry_api, + :opentelemetry_exporter, + :opentelemetry_semantic_conventions + ] + + assert reason in [:not_loaded] or match?({:version_too_old, _, _}, reason) + end + end + end + end + + describe "tracing_compatible?/0" do + test "returns boolean" do + result = VersionChecker.tracing_compatible?() + assert is_boolean(result) + end + end + + describe "version comparison logic" do + test "module exports expected public functions" do + # Test that the module defines the required public functions + assert VersionChecker.__info__(:functions) |> Keyword.has_key?(:check_compatibility) + assert VersionChecker.__info__(:functions) |> Keyword.has_key?(:tracing_compatible?) + end + end +end diff --git a/test_integrations/legacy_otel/lib/legacy_otel.ex b/test_integrations/legacy_otel/lib/legacy_otel.ex new file mode 100644 index 00000000..a701fef3 --- /dev/null +++ b/test_integrations/legacy_otel/lib/legacy_otel.ex @@ -0,0 +1,26 @@ +defmodule LegacyOtel do + def get_otel_versions do + Application.loaded_applications() + |> Enum.filter(fn {app, _desc, _vsn} -> + app in [:opentelemetry, :opentelemetry_api, :opentelemetry_exporter, :opentelemetry_semantic_conventions] + end) + |> Enum.map(fn {app, _desc, vsn} -> {app, to_string(vsn)} end) + |> Map.new() + end + + def test_sentry_otel_integration do + span_processor_defined? = Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) + sampler_defined? = Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) + span_record_defined? = Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + + version_compatible? = Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() + + %{ + span_processor_defined: span_processor_defined?, + sampler_defined: sampler_defined?, + span_record_defined: span_record_defined?, + version_compatible: version_compatible?, + loaded_versions: get_otel_versions() + } + end +end diff --git a/test_integrations/legacy_otel/mix.exs b/test_integrations/legacy_otel/mix.exs new file mode 100644 index 00000000..19059c9f --- /dev/null +++ b/test_integrations/legacy_otel/mix.exs @@ -0,0 +1,33 @@ +defmodule LegacyOtel.MixProject do + use Mix.Project + + def project do + [ + app: :legacy_otel, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:sentry, path: "../.."}, + {:hackney, "~> 1.18"}, + {:jason, "~> 1.1", optional: true}, + {:opentelemetry, "~> 1.3.0"}, + {:opentelemetry_api, "~> 1.2"}, + {:opentelemetry_exporter, "~> 1.4.0"}, + {:opentelemetry_semantic_conventions, "~> 0.2"} + ] + end +end diff --git a/test_integrations/legacy_otel/mix.lock b/test_integrations/legacy_otel/mix.lock new file mode 100644 index 00000000..50c3a797 --- /dev/null +++ b/test_integrations/legacy_otel/mix.lock @@ -0,0 +1,24 @@ +%{ + "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, + "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, + "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, + "opentelemetry": {:hex, :opentelemetry, "1.3.1", "f0a342a74379e3540a634e7047967733da4bc8b873ec9026e224b2bd7369b1fc", [:rebar3], [{:opentelemetry_api, "~> 1.2.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "de476b2ac4faad3e3fe3d6e18b35dec9cb338c3b9910c2ce9317836dacad3483"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.4.1", "5c80c3a22ec084b4e0a9ac7d39a435b332949b2dceec9fb19f5c5d2ca8ae1d56", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.3", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "5a0ff6618b0f7370bd10b50e64099a4c2aa52145ae6567cccf7d76ba2d32e079"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.28.0", "c39bf21f67c2d124ae905454fad00f27e625917e8ab1009146e916e1df6ab275", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3ab058c3f9457fffca916729587415f0ddc822048a0e5b5e2694918556d92df1"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, +} diff --git a/test_integrations/legacy_otel/test/legacy_otel_test.exs b/test_integrations/legacy_otel/test/legacy_otel_test.exs new file mode 100644 index 00000000..53b04e02 --- /dev/null +++ b/test_integrations/legacy_otel/test/legacy_otel_test.exs @@ -0,0 +1,118 @@ +defmodule LegacyOtelTest do + use ExUnit.Case, async: true + + import ExUnit.CaptureLog + + describe "OpenTelemetry version compatibility" do + test "older OpenTelemetry versions are detected as incompatible" do + refute Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() + + assert {:error, {:incompatible_versions, errors}} = + Sentry.OpenTelemetry.VersionChecker.check_compatibility() + + assert length(errors) > 0 + error_deps = Enum.map(errors, fn {dep, _reason} -> dep end) + + expected_deps = [:opentelemetry, :opentelemetry_api, :opentelemetry_exporter, :opentelemetry_semantic_conventions] + assert Enum.any?(expected_deps, fn dep -> dep in error_deps end) + end + + test "Sentry OpenTelemetry modules are not defined with older versions" do + refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) + refute Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) + refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + end + + test "LegacyOtel.test_sentry_otel_integration/0 returns expected results" do + result = LegacyOtel.test_sentry_otel_integration() + + assert is_map(result) + assert Map.has_key?(result, :span_processor_defined) + assert Map.has_key?(result, :sampler_defined) + assert Map.has_key?(result, :span_record_defined) + assert Map.has_key?(result, :version_compatible) + assert Map.has_key?(result, :loaded_versions) + + refute result.span_processor_defined + refute result.sampler_defined + refute result.span_record_defined + refute result.version_compatible + + assert is_map(result.loaded_versions) + assert Map.has_key?(result.loaded_versions, :opentelemetry) + end + + test "loaded OpenTelemetry versions are older than required" do + versions = LegacyOtel.get_otel_versions() + + assert Map.get(versions, :opentelemetry) == "1.3.1" + assert Map.get(versions, :opentelemetry_api) == "1.2.2" + assert Map.get(versions, :opentelemetry_exporter) == "1.4.1" + assert Map.get(versions, :opentelemetry_semantic_conventions) == "0.2.0" + end + end + + describe "Sentry configuration with older OpenTelemetry" do + test "tracing should be disabled in Sentry config" do + refute Sentry.Config.tracing? + end + + test "Config.validate! warns when traces_sample_rate is set but dependencies are not satisfied" do + config = [ + dsn: "https://public@sentry.example.com/1", + traces_sample_rate: 0.5 + ] + + log_output = capture_log(fn -> + validated_config = Sentry.Config.validate!(config) + + assert Keyword.get(validated_config, :traces_sample_rate) == 0.5 + dsn = Keyword.get(validated_config, :dsn) + assert dsn.original_dsn == "https://public@sentry.example.com/1" + end) + + assert log_output =~ "Sentry tracing is configured with traces_sample_rate: 0.5" + assert log_output =~ "but the required OpenTelemetry dependencies are not satisfied" + assert log_output =~ "Tracing will be disabled" + assert log_output =~ "opentelemetry (>= 1.5.0)" + assert log_output =~ "opentelemetry_api (>= 1.4.0)" + assert log_output =~ "opentelemetry_exporter (>= 1.0.0)" + assert log_output =~ "opentelemetry_semantic_conventions (>= 1.27.0)" + end + + test "Config.validate! does not warn when traces_sample_rate is nil" do + config = [ + dsn: "https://public@sentry.example.com/1", + traces_sample_rate: nil + ] + + log_output = capture_log(fn -> + validated_config = Sentry.Config.validate!(config) + + assert Keyword.get(validated_config, :traces_sample_rate) == nil + dsn = Keyword.get(validated_config, :dsn) + assert dsn.original_dsn == "https://public@sentry.example.com/1" + end) + + refute log_output =~ "Sentry tracing is configured" + refute log_output =~ "OpenTelemetry dependencies are not satisfied" + end + + test "Config.validate! does not warn when traces_sample_rate is not set" do + config = [ + dsn: "https://public@sentry.example.com/1" + ] + + log_output = capture_log(fn -> + validated_config = Sentry.Config.validate!(config) + + assert Keyword.get(validated_config, :traces_sample_rate) == nil + dsn = Keyword.get(validated_config, :dsn) + assert dsn.original_dsn == "https://public@sentry.example.com/1" + end) + + refute log_output =~ "Sentry tracing is configured" + refute log_output =~ "OpenTelemetry dependencies are not satisfied" + end + end +end diff --git a/test_integrations/legacy_otel/test/test_helper.exs b/test_integrations/legacy_otel/test/test_helper.exs new file mode 100644 index 00000000..869559e7 --- /dev/null +++ b/test_integrations/legacy_otel/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/test_integrations/phoenix_app/mix.exs b/test_integrations/phoenix_app/mix.exs index 52e6a1b4..d30a5de8 100644 --- a/test_integrations/phoenix_app/mix.exs +++ b/test_integrations/phoenix_app/mix.exs @@ -69,7 +69,7 @@ defmodule PhoenixApp.MixProject do {:opentelemetry, "~> 1.5"}, {:opentelemetry_api, "~> 1.4"}, - {:opentelemetry_exporter, "~> 1.0"}, + {:opentelemetry_exporter, "~> 1.8"}, {:opentelemetry_semantic_conventions, "~> 1.27"}, {:opentelemetry_bandit, "~> 0.1"}, {:opentelemetry_phoenix, "~> 2.0"}, diff --git a/test_integrations/umbrella/mix.lock b/test_integrations/umbrella/mix.lock index 4aea5a19..ad4e6b16 100644 --- a/test_integrations/umbrella/mix.lock +++ b/test_integrations/umbrella/mix.lock @@ -1,13 +1,13 @@ %{ - "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, - "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, }