diff --git a/lib/sentry/opentelemetry/span_processor.ex b/lib/sentry/opentelemetry/span_processor.ex index ae0ac114..a295a7b5 100644 --- a/lib/sentry/opentelemetry/span_processor.ex +++ b/lib/sentry/opentelemetry/span_processor.ex @@ -1,6 +1,11 @@ defmodule Sentry.OpenTelemetry.SpanProcessor do @moduledoc false + require OpenTelemetry.SemConv.ClientAttributes, as: ClientAttributes + require OpenTelemetry.SemConv.Incubating.DBAttributes, as: DBAttributes + require OpenTelemetry.SemConv.Incubating.HTTPAttributes, as: HTTPAttributes + require OpenTelemetry.SemConv.Incubating.URLAttributes, as: URLAttributes + @behaviour :otel_span_processor require Logger @@ -23,10 +28,9 @@ defmodule Sentry.OpenTelemetry.SpanProcessor do SpanStorage.update_span(span_record) if span_record.parent_span_id == nil do - root_span = SpanStorage.get_root_span(span_record.span_id) - child_spans = SpanStorage.get_child_spans(span_record.span_id) - - transaction = build_transaction(root_span, child_spans) + root_span_record = SpanStorage.get_root_span(span_record.span_id) + child_span_records = SpanStorage.get_child_spans(span_record.span_id) + transaction = build_transaction(root_span_record, child_span_records) result = case Sentry.send_transaction(transaction) do @@ -54,350 +58,104 @@ defmodule Sentry.OpenTelemetry.SpanProcessor do :ok end - defp build_transaction(%SpanRecord{origin: :undefined} = root_span, child_spans) do - Transaction.new(%{ - transaction: root_span.name, - start_timestamp: root_span.start_time, - timestamp: root_span.end_time, - contexts: %{ - trace: build_trace_context(root_span) - }, - spans: [build_span(root_span) | Enum.map(child_spans, &build_span(&1))] - }) - end - - defp build_transaction(%SpanRecord{origin: "opentelemetry_ecto"} = root_span, child_spans) do - Transaction.new(%{ - transaction: root_span.name, - start_timestamp: root_span.start_time, - timestamp: root_span.end_time, - transaction_info: %{ - source: "component" - }, - contexts: %{ - trace: build_trace_context(root_span) - }, - data: root_span.attributes, - measurements: %{}, - spans: Enum.map(child_spans, &build_span(&1)) - }) - end + defp build_transaction(root_span_record, child_span_records) do + root_span = build_span(root_span_record) + child_spans = Enum.map(child_span_records, &build_span(&1)) - defp build_transaction( - %SpanRecord{attributes: attributes, origin: "opentelemetry_phoenix"} = root_span, - child_spans - ) - when map_size(attributes) > 0 do Transaction.new(%{ - transaction: "#{attributes["phoenix.plug"]}##{attributes["phoenix.action"]}", - start_timestamp: root_span.start_time, - timestamp: root_span.end_time, - transaction_info: %{ - source: "view" - }, - contexts: %{ - trace: build_trace_context(root_span) - }, - request: %{ - url: url_from_attributes(attributes), - method: attributes["http.method"], - headers: %{ - "User-Agent" => attributes["http.user_agent"] - }, - env: %{ - "SERVER_NAME" => attributes["net.host.name"], - "SERVER_PORT" => attributes["net.host.port"] - } - }, - data: %{ - "http.response.status_code" => attributes["http.status_code"], - "method" => attributes["http.method"], - "path" => attributes["http.target"], - "params" => %{ - "controller" => attributes["phoenix.plug"], - "action" => attributes["phoenix.action"] - } - }, - measurements: %{}, - spans: Enum.map(child_spans, &build_span(&1)) - }) - end - - defp build_transaction( - %SpanRecord{attributes: attributes, origin: "opentelemetry_phoenix"} = root_span, - child_spans - ) - when map_size(attributes) == 0 do - Transaction.new(%{ - transaction: root_span.name, - start_timestamp: root_span.start_time, - timestamp: root_span.end_time, - transaction_info: %{ - source: "view" - }, - contexts: %{ - trace: build_trace_context(root_span) - }, - spans: Enum.map(child_spans, &build_span(&1)) - }) - end - - defp build_transaction( - %SpanRecord{attributes: attributes, origin: "opentelemetry_bandit"} = root_span, - child_spans - ) do - Transaction.new(%{ - start_timestamp: root_span.start_time, - timestamp: root_span.end_time, - transaction: attributes["http.target"], - transaction_info: %{ - source: "url" - }, + span_id: root_span.span_id, + transaction: root_span_record.name, + transaction_info: %{source: :custom}, contexts: %{ - trace: build_trace_context(root_span) - }, - request: %{ - url: attributes["http.url"], - method: attributes["http.method"], - headers: %{ - "User-Agent" => attributes["http.user_agent"] - }, - env: %{ - "SERVER_NAME" => attributes["net.peer.name"], - "SERVER_PORT" => attributes["net.peer.port"] - } + trace: build_trace_context(root_span_record), + otel: build_otel_context(root_span_record) }, - measurements: %{}, - spans: Enum.map(child_spans, &build_span(&1)) + spans: [root_span | child_spans] }) end - defp build_transaction(%SpanRecord{origin: "opentelemetry_oban"} = root_span, child_spans) do - Transaction.new(%{ - transaction: root_span.name |> String.split(" ") |> List.first(), - start_timestamp: root_span.start_time, - timestamp: root_span.end_time, - transaction_info: %{ - source: "task" - }, - contexts: %{ - trace: build_trace_context(root_span) - }, - measurements: %{}, - spans: Enum.map(child_spans, &build_span(&1)) - }) - end + defp build_trace_context(span_record) do + {op, description} = get_op_description(span_record) - defp build_trace_context( - %SpanRecord{origin: "opentelemetry_phoenix", attributes: attributes} = root_span - ) - when map_size(attributes) > 0 do %{ - trace_id: root_span.trace_id, - span_id: root_span.span_id, - parent_span_id: nil, - op: "http.server", - origin: root_span.origin, - status: status_from_attributes(attributes), - data: %{ - "http.response.status_code" => attributes["http.status_code"] - } + trace_id: span_record.trace_id, + span_id: span_record.span_id, + parent_span_id: span_record.parent_span_id, + op: op, + description: description, + origin: span_record.origin, + data: span_record.attributes } end - defp build_trace_context( - %SpanRecord{origin: "opentelemetry_phoenix", attributes: attributes} = root_span - ) - when map_size(attributes) == 0 do - %{ - trace_id: root_span.trace_id, - span_id: root_span.span_id, - parent_span_id: nil, - op: "http.server.live", - description: root_span.name, - origin: root_span.origin - } - end + defp build_otel_context(span_record), do: span_record.attributes - defp build_trace_context( - %SpanRecord{origin: "opentelemetry_ecto", attributes: attributes} = root_span - ) do - %{ - trace_id: root_span.trace_id, - span_id: root_span.span_id, - parent_span_id: root_span.parent_span_id, - op: "db.#{attributes["db.type"]}.ecto", - description: attributes["db.statement"] || root_span.name, - origin: root_span.origin, - data: attributes - } - end + defp get_op_description(%{attributes: %{unquote(to_string(HTTPAttributes.http_request_method())) => http_request_method}} = span_record) do + op = "http.#{span_record.kind}" + client_address = Map.get(span_record.attributes, to_string(ClientAttributes.client_address())) + url_path = Map.get(span_record.attributes, to_string(URLAttributes.url_path())) - defp build_trace_context( - %SpanRecord{ - origin: "opentelemetry_oban", - attributes: %{"oban.plugin" => Oban.Stager} = _attributes - } = root_span - ) do - %{ - trace_id: root_span.trace_id, - span_id: root_span.span_id, - parent_span_id: root_span.parent_span_id, - op: "queue.process", - origin: root_span.origin, - data: %{ - "oban.plugin" => "stager" - } - } - end + description = + to_string(http_request_method) <> + (client_address && " from #{client_address}" || "") <> + (url_path && " #{url_path}" || "") - defp build_trace_context( - %SpanRecord{origin: "opentelemetry_oban", attributes: attributes} = root_span - ) do - now = DateTime.utc_now() - {:ok, scheduled_at, _} = DateTime.from_iso8601(attributes["oban.job.scheduled_at"]) + {op, description} + end - latency = DateTime.diff(now, scheduled_at, :millisecond) + defp get_op_description(%{attributes: %{unquote(to_string(DBAttributes.db_system())) => _db_system}} = span_record) do + db_query_text = Map.get(span_record.attributes, to_string(DBAttributes.db_statement())) - %{ - trace_id: root_span.trace_id, - span_id: root_span.span_id, - parent_span_id: root_span.parent_span_id, - op: "queue.process", - origin: root_span.origin, - data: %{ - id: attributes["oban.job.job_id"], - queue: attributes["messaging.destination"], - retry_count: attributes["oban.job.attempt"], - latency: latency - } - } + {"db", db_query_text} end - defp build_trace_context(%SpanRecord{attributes: attributes} = root_span) do - %{ - trace_id: root_span.trace_id, - span_id: root_span.span_id, - parent_span_id: nil, - op: root_span.name, - origin: root_span.origin, - data: attributes - } + defp get_op_description(span_record) do + {span_record.name, span_record.name} end - defp build_span( - %SpanRecord{origin: "opentelemetry_phoenix", attributes: attributes} = span_record - ) - when map_size(attributes) == 0 do - %Span{ - op: "http.server.live", - description: span_record.name, - start_timestamp: span_record.start_time, - timestamp: span_record.end_time, - trace_id: span_record.trace_id, - span_id: span_record.span_id, - parent_span_id: span_record.parent_span_id, - origin: span_record.origin - } - end + defp build_span(span_record) do + {op, description} = get_op_description(span_record) - defp build_span( - %SpanRecord{origin: "opentelemetry_phoenix", attributes: attributes} = span_record - ) do %Span{ - op: "#{attributes["phoenix.plug"]}##{attributes["phoenix.action"]}", + op: op, + description: description, start_timestamp: span_record.start_time, timestamp: span_record.end_time, trace_id: span_record.trace_id, span_id: span_record.span_id, parent_span_id: span_record.parent_span_id, - description: attributes["http.route"], - origin: span_record.origin + origin: span_record.origin, + data: Map.put(span_record.attributes, "otel.kind", span_record.kind), + status: span_status(span_record) } end - defp build_span(%SpanRecord{origin: "phoenix_app"} = span_record) do - %Span{ - trace_id: span_record.trace_id, - op: span_record.name, - start_timestamp: span_record.start_time, - timestamp: span_record.end_time, - span_id: span_record.span_id, - parent_span_id: span_record.parent_span_id - } + defp span_status(%{attributes: %{unquote(to_string(HTTPAttributes.http_response_status_code())) => http_response_status_code}}) do + to_status(http_response_status_code) end - defp build_span(%SpanRecord{origin: "opentelemetry_bandit"} = span_record) do - %Span{ - trace_id: span_record.trace_id, - op: span_record.name, - start_timestamp: span_record.start_time, - timestamp: span_record.end_time, - span_id: span_record.span_id, - parent_span_id: span_record.parent_span_id, - description: span_record.name, - origin: span_record.origin - } - end + defp span_status(_span_record), do: nil - defp build_span(%SpanRecord{origin: "opentelemetry_ecto", attributes: attributes} = span_record) do - %Span{ - trace_id: span_record.trace_id, - span_id: span_record.span_id, - parent_span_id: span_record.parent_span_id, - op: "db.#{attributes["db.type"]}.ecto", - description: attributes["db.statement"] || span_record.name, - origin: span_record.origin, - start_timestamp: span_record.start_time, - timestamp: span_record.end_time, - data: attributes - } - end + # WebSocket upgrade spans doesn't have a HTTP status + defp to_status(nil), do: nil - defp build_span(%SpanRecord{origin: :undefined, attributes: _attributes} = span_record) do - %Span{ - trace_id: span_record.trace_id, - op: span_record.name, - start_timestamp: span_record.start_time, - timestamp: span_record.end_time, - span_id: span_record.span_id, - parent_span_id: span_record.parent_span_id, - # Add origin to match other span types - origin: span_record.origin - } - end + defp to_status(status) when status in 200..299, do: "ok" - defp url_from_attributes(attributes) do - URI.to_string(%URI{ - scheme: attributes["http.scheme"], - host: attributes["net.host.name"], - port: attributes["net.host.port"], - path: attributes["http.target"] - }) + for {status, string} <- %{ + 400 => "invalid_argument", + 401 => "unauthenticated", + 403 => "permission_denied", + 404 => "not_found", + 409 => "already_exists", + 429 => "resource_exhausted", + 499 => "cancelled", + 500 => "internal_error", + 501 => "unimplemented", + 503 => "unavailable", + 504 => "deadline_exceeded" + } do + defp to_status(unquote(status)), do: unquote(string) end - defp status_from_attributes(%{"http.status_code" => status_code}) do - cond do - status_code in 200..299 -> - "ok" - - status_code in [400, 401, 403, 404, 409, 429, 499, 500, 501, 503, 504] -> - %{ - 400 => "invalid_argument", - 401 => "unauthenticated", - 403 => "permission_denied", - 404 => "not_found", - 409 => "already_exists", - 429 => "resource_exhausted", - 499 => "cancelled", - 500 => "internal_error", - 501 => "unimplemented", - 503 => "unavailable", - 504 => "deadline_exceeded" - }[status_code] - - true -> - "unknown_error" - end - end + defp to_status(_any), do: "unknown_error" end diff --git a/lib/sentry/test.ex b/lib/sentry/test.ex index 41d71258..d77065f8 100644 --- a/lib/sentry/test.ex +++ b/lib/sentry/test.ex @@ -361,7 +361,7 @@ defmodule Sentry.Test do iex> Sentry.Test.start_collecting_sentry_reports() :ok - iex> Sentry.send_transaction(%Sentry.Transaction{}) + iex> Sentry.send_transaction(Sentry.Transaction.new(%{span_id: "123", spans: []})) {:ok, ""} iex> [%Sentry.Transaction{}] = Sentry.Test.pop_sentry_transactions() diff --git a/lib/sentry/transaction.ex b/lib/sentry/transaction.ex index 3d31239e..00ad0717 100644 --- a/lib/sentry/transaction.ex +++ b/lib/sentry/transaction.ex @@ -1,50 +1,60 @@ defmodule Sentry.Transaction do @type t() :: %__MODULE__{} - alias Sentry.{UUID} + alias Sentry.{Config, UUID} + + @enforce_keys ~w(event_id span_id spans)a defstruct [ :event_id, - :start_timestamp, - :timestamp, + :environment, + :span_id, :transaction, :transaction_info, - :status, :contexts, - :request, :measurements, - spans: [], + :spans, type: "transaction" ] def new(attrs) do - struct(__MODULE__, Map.put(attrs, :event_id, UUID.uuid4_hex())) + struct!( + __MODULE__, + attrs + |> Map.put(:event_id, UUID.uuid4_hex()) + |> Map.put(:environment, Config.environment_name()) + ) end # Used to then encode the returned map to JSON. @doc false def to_map(%__MODULE__{} = transaction) do - Map.put( - Map.from_struct(transaction), - :spans, - Enum.map(transaction.spans, &Sentry.Span.to_map(&1)) - ) + transaction_attrs = Map.take(transaction, [:event_id, :environment, :transaction, :transaction_info, :contexts, :measurements, :type]) + {[root_span], child_spans} = Enum.split_with(transaction.spans, &is_nil(&1.parent_span_id)) + + root_span + |> Sentry.Span.to_map() + |> Map.put(:spans, Enum.map(child_spans, &Sentry.Span.to_map/1)) + |> Map.drop([:description]) + |> Map.merge(transaction_attrs) end end defmodule Sentry.Span do + @enforce_keys ~w(span_id trace_id start_timestamp timestamp)a + defstruct [ - :op, + :trace_id, + :span_id, + :parent_span_id, :start_timestamp, :timestamp, :description, - :span_id, - :parent_span_id, - :trace_id, + :op, + :status, :tags, :data, - :origin, - :status + :origin ] # Used to then encode the returned map to JSON. diff --git a/mix.exs b/mix.exs index fa54051c..d805d20b 100644 --- a/mix.exs +++ b/mix.exs @@ -115,7 +115,8 @@ defmodule Sentry.Mixfile do # Required by Tracing {:opentelemetry, "~> 1.5"}, - {:opentelemetry_api, "~> 1.3"} + {:opentelemetry_api, "~> 1.3"}, + {:opentelemetry_semantic_conventions, "~> 1.0"} ] end diff --git a/mix.lock b/mix.lock index 634fc095..522c8a6b 100644 --- a/mix.lock +++ b/mix.lock @@ -34,7 +34,7 @@ "oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"}, "opentelemetry": {:hex, :opentelemetry, "1.5.0", "7dda6551edfc3050ea4b0b40c0d2570423d6372b97e9c60793263ef62c53c3c2", [:rebar3], [{:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "cdf4f51d17b592fc592b9a75f86a6f808c23044ba7cf7b9534debbcc5c23b0ee"}, "opentelemetry_api": {:hex, :opentelemetry_api, "1.4.0", "63ca1742f92f00059298f478048dfb826f4b20d49534493d6919a0db39b6db04", [:mix, :rebar3], [], "hexpm", "3dfbbfaa2c2ed3121c5c483162836c4f9027def469c41578af5ef32589fcfc58"}, - "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, diff --git a/test/envelope_test.exs b/test/envelope_test.exs index 35275957..4f409ef5 100644 --- a/test/envelope_test.exs +++ b/test/envelope_test.exs @@ -117,14 +117,14 @@ defmodule Sentry.EnvelopeTest do test "works with transactions" do put_test_config(environment_name: "test") - spans = [ + root_span = %Sentry.Span{ start_timestamp: 1_588_601_261.481_961, timestamp: 1_588_601_261.488_901, description: "GET /sockjs-node/info", op: "http", span_id: "b01b9f6349558cd1", - parent_span_id: "b0e6f15b45c36b12", + parent_span_id: nil, trace_id: "1e57b752bc6e4544bbaa246cd1d05dee", tags: %{"http.status_code" => "200"}, data: %{ @@ -133,23 +133,27 @@ defmodule Sentry.EnvelopeTest do "type" => "xhr", "method" => "GET" } - }, - %Sentry.Span{ - start_timestamp: 1_588_601_261.535_386, - timestamp: 1_588_601_261.544_196, - description: "Vue ", - op: "update", - span_id: "b980d4dec78d7344", - parent_span_id: "9312d0d18bf51736", - trace_id: "1e57b752bc6e4544bbaa246cd1d05dee" } - ] - transaction = %Sentry.Transaction{ - start_timestamp: System.system_time(:second), - timestamp: System.system_time(:second), - spans: spans - } + child_spans = + [ + %Sentry.Span{ + start_timestamp: 1_588_601_261.535_386, + timestamp: 1_588_601_261.544_196, + description: "Vue ", + op: "update", + span_id: "b980d4dec78d7344", + parent_span_id: "9312d0d18bf51736", + trace_id: "1e57b752bc6e4544bbaa246cd1d05dee" + } + ] + + transaction = + Sentry.Transaction.new(%{ + span_id: root_span.span_id, + spans: [root_span | child_spans], + transaction: "test-transaction" + }) envelope = Envelope.from_transaction(transaction) @@ -159,16 +163,13 @@ defmodule Sentry.EnvelopeTest do assert {:ok, decoded_transaction} = Jason.decode(transaction_line) assert decoded_transaction["type"] == "transaction" - assert decoded_transaction["start_timestamp"] == transaction.start_timestamp - assert decoded_transaction["timestamp"] == transaction.timestamp - - assert [span1, span2] = decoded_transaction["spans"] + assert decoded_transaction["start_timestamp"] == root_span.start_timestamp + assert decoded_transaction["timestamp"] == root_span.timestamp - assert span1["start_timestamp"] == List.first(spans).start_timestamp - assert span1["timestamp"] == List.first(spans).timestamp + assert [span] = decoded_transaction["spans"] - assert span2["start_timestamp"] == List.last(spans).start_timestamp - assert span2["timestamp"] == List.last(spans).timestamp + assert span["start_timestamp"] == List.first(child_spans).start_timestamp + assert span["timestamp"] == List.first(child_spans).timestamp end end diff --git a/test/sentry/client_report/sender_test.exs b/test/sentry/client_report/sender_test.exs index a349018c..d9d6f551 100644 --- a/test/sentry/client_report/sender_test.exs +++ b/test/sentry/client_report/sender_test.exs @@ -4,7 +4,7 @@ defmodule Sentry.ClientReportTest do import Sentry.TestHelpers alias Sentry.ClientReport.Sender - alias Sentry.{Event, Transaction} + alias Sentry.{Event, Transaction, Span} setup do original_retries = @@ -19,6 +19,8 @@ defmodule Sentry.ClientReportTest do %{bypass: bypass} end + @span_id Sentry.UUID.uuid4_hex() + describe "record_discarded_events/2 + flushing" do test "succefully records the discarded event to the client report", %{bypass: bypass} do start_supervised!({Sender, name: :test_client_report}) @@ -28,10 +30,16 @@ defmodule Sentry.ClientReportTest do event_id: Sentry.UUID.uuid4_hex(), timestamp: "2024-10-12T13:21:13" }, - %Transaction{ - event_id: Sentry.UUID.uuid4_hex(), - timestamp: "2024-10-12T13:21:13" - } + Transaction.new(%{ + span_id: @span_id, + transaction: "test-transaction", + spans: [%Span{ + span_id: @span_id, + trace_id: Sentry.UUID.uuid4_hex(), + start_timestamp: "2024-10-12T13:21:13", + timestamp: "2024-10-12T13:21:13" + }] + }) ] assert :ok = Sender.record_discarded_events(:before_send, events, :test_client_report) diff --git a/test/sentry/opentelemetry/span_processor_test.exs b/test/sentry/opentelemetry/span_processor_test.exs index 42283706..a7b29626 100644 --- a/test/sentry/opentelemetry/span_processor_test.exs +++ b/test/sentry/opentelemetry/span_processor_test.exs @@ -44,16 +44,18 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do assert [%Sentry.Transaction{} = transaction] = Sentry.Test.pop_sentry_transactions() - assert_valid_iso8601(transaction.timestamp) - assert_valid_iso8601(transaction.start_timestamp) - assert transaction.timestamp > transaction.start_timestamp - assert length(transaction.spans) == 1 - - assert_valid_trace_id(transaction.contexts.trace.trace_id) - - assert [span] = transaction.spans - - assert span.op == "child_instrumented_function_one" + transaction_data = Sentry.Transaction.to_map(transaction) + + assert transaction_data.event_id + assert transaction_data.environment == "test" + assert transaction_data.type == "transaction" + assert transaction_data.op == "child_instrumented_function_one" + assert transaction_data.transaction_info == %{source: :custom} + assert_valid_iso8601(transaction_data.timestamp) + assert_valid_iso8601(transaction_data.start_timestamp) + assert transaction_data.timestamp > transaction_data.start_timestamp + assert_valid_trace_id(transaction_data.contexts.trace.trace_id) + assert length(transaction_data.spans) == 0 end test "sends captured spans as transactions with child spans" do @@ -64,12 +66,16 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do TestEndpoint.instrumented_function() assert [%Sentry.Transaction{} = transaction] = Sentry.Test.pop_sentry_transactions() - assert_valid_iso8601(transaction.timestamp) - assert_valid_iso8601(transaction.start_timestamp) - assert transaction.timestamp > transaction.start_timestamp - assert length(transaction.spans) == 3 - [root_span, child_span_one, child_span_two] = transaction.spans + transaction_data = Sentry.Transaction.to_map(transaction) + + assert transaction_data.op == "instrumented_function" + assert_valid_iso8601(transaction_data.timestamp) + assert_valid_iso8601(transaction_data.start_timestamp) + assert transaction_data.timestamp > transaction_data.start_timestamp + assert length(transaction_data.spans) == 2 + + [child_span_one, child_span_two] = transaction_data.spans assert child_span_one.op == "child_instrumented_function_one" assert child_span_two.op == "child_instrumented_function_two" assert child_span_one.parent_span_id == transaction.contexts.trace.span_id @@ -82,10 +88,10 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do assert child_span_one.timestamp > child_span_one.start_timestamp assert child_span_two.timestamp > child_span_two.start_timestamp - assert root_span.timestamp >= child_span_one.timestamp - assert root_span.timestamp >= child_span_two.timestamp - assert root_span.start_timestamp <= child_span_one.start_timestamp - assert root_span.start_timestamp <= child_span_two.start_timestamp + assert transaction_data.timestamp >= child_span_one.timestamp + assert transaction_data.timestamp >= child_span_two.timestamp + assert transaction_data.start_timestamp <= child_span_one.start_timestamp + assert transaction_data.start_timestamp <= child_span_two.start_timestamp assert_valid_trace_id(transaction.contexts.trace.trace_id) assert_valid_trace_id(child_span_one.trace_id) diff --git a/test/sentry_test.exs b/test/sentry_test.exs index 93bbaa0b..4f53b542 100644 --- a/test/sentry_test.exs +++ b/test/sentry_test.exs @@ -240,9 +240,16 @@ defmodule SentryTest do setup do transaction = Sentry.Transaction.new(%{ + span_id: "root-span", transaction: "test-transaction", - start_timestamp: System.system_time(:second), - timestamp: System.system_time(:second) + spans: [ + %Sentry.Span{ + span_id: "root-span", + trace_id: "trace-id", + start_timestamp: 1_234_567_891.123_456, + timestamp: 1_234_567_891.123_456 + } + ] }) {:ok, transaction: transaction} diff --git a/test_integrations/phoenix_app/lib/phoenix_app/application.ex b/test_integrations/phoenix_app/lib/phoenix_app/application.ex index b831628b..442c43e2 100644 --- a/test_integrations/phoenix_app/lib/phoenix_app/application.ex +++ b/test_integrations/phoenix_app/lib/phoenix_app/application.ex @@ -14,7 +14,7 @@ defmodule PhoenixApp.Application do }) # OpentelemetryBandit.setup() - OpentelemetryPhoenix.setup() + OpentelemetryPhoenix.setup(adapter: :bandit) OpentelemetryOban.setup() OpentelemetryEcto.setup([:phoenix_app, :repo], db_statement: :enabled) diff --git a/test_integrations/phoenix_app/mix.exs b/test_integrations/phoenix_app/mix.exs index 6244a049..67926e11 100644 --- a/test_integrations/phoenix_app/mix.exs +++ b/test_integrations/phoenix_app/mix.exs @@ -66,7 +66,10 @@ defmodule PhoenixApp.MixProject do {:bypass, "~> 2.1", only: :test}, {:opentelemetry, "~> 1.5"}, {:opentelemetry_api, "~> 1.3"}, - {:opentelemetry_phoenix, "~> 1.2"}, + {:opentelemetry_phoenix, "~> 2.0"}, + # TODO: Remove when opentelemetry_oban upstream has been updated + # from opentelemetry_semantic_conventions 0.2 to 1.0 + {:opentelemetry_semantic_conventions, "~> 1.0", override: true}, {:opentelemetry_oban, "~> 1.1"}, # {:opentelemetry_bandit, "~> 0.1.4", github: "solnic/opentelemetry-bandit", depth: 1}, {:opentelemetry_ecto, "~> 1.2"}, diff --git a/test_integrations/phoenix_app/mix.lock b/test_integrations/phoenix_app/mix.lock index 3160caed..fec567f3 100644 --- a/test_integrations/phoenix_app/mix.lock +++ b/test_integrations/phoenix_app/mix.lock @@ -39,10 +39,11 @@ "opentelemetry_bandit": {:git, "https://github.com/solnic/opentelemetry-bandit.git", "1e00505fb3bb02001a3400f8a807cd1c7f7f957d", []}, "opentelemetry_ecto": {:hex, :opentelemetry_ecto, "1.2.0", "2382cb47ddc231f953d3b8263ed029d87fbf217915a1da82f49159d122b64865", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "70dfa2e79932e86f209df00e36c980b17a32f82d175f0068bf7ef9a96cf080cf"}, "opentelemetry_oban": {:hex, :opentelemetry_oban, "1.1.1", "519e9ba60d3dc3483ad2df3fade131d47056e0dae74f0724c8a40b9718f089d1", [:mix], [{:oban, "~> 2.0", [hex: :oban, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.1", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ae6aed431626a94a4bb6bf5b268247ced687ec8f99eced6887e3754f9d3a2089"}, - "opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "1.2.0", "b8a53ee595b24970571a7d2fcaef3e4e1a021c68e97cac163ca5d9875fad5e9f", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "acab991d14ed3efc3f780c5a20cabba27149cf731005b1cc6454c160859debe5"}, + "opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "2.0.0", "3a22f620a26613ba02e7289238da145c2ddcd58bd37b780b200080139d24b176", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.1", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:otel_http, "~> 0.2", [hex: :otel_http, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2c0969c561a87703cda64e9f0c37e9dec6dceee11c2d2eafef8d3f4138ec364"}, "opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.3.0", "ef5b2059403a1e2b2d2c65914e6962e56371570b8c3ab5323d7a8d3444fb7f84", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "7243cb6de1523c473cba5b1aefa3f85e1ff8cc75d08f367104c1e11919c8c029"}, - "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"}, "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.2", "410ab4d76b0921f42dbccbe5a7c831b8125282850be649ee1f70050d3961118a", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "641ab469deb181957ac6d59bce6e1321d5fe2a56df444fc9c19afcad623ab253"}, + "otel_http": {:hex, :otel_http, "0.2.0", "b17385986c7f1b862f5d577f72614ecaa29de40392b7618869999326b9a61d8a", [:rebar3], [], "hexpm", "f2beadf922c8cfeb0965488dd736c95cc6ea8b9efce89466b3904d317d7cc717"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, diff --git a/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs b/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs index ec4c98f0..5280af78 100644 --- a/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs +++ b/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs @@ -28,17 +28,16 @@ defmodule Sentry.Integrations.Phoenix.ObanTest do [transaction] = transactions - assert transaction.transaction == "Sentry.Integrations.Phoenix.ObanTest.TestWorker" - assert transaction.transaction_info == %{source: "task"} + assert transaction.transaction == "Sentry.Integrations.Phoenix.ObanTest.TestWorker process" + assert transaction.transaction_info == %{source: :custom} trace = transaction.contexts.trace assert trace.origin == "opentelemetry_oban" - assert trace.op == "queue.process" - assert trace.data.id - assert trace.data.queue == "default" - assert trace.data.retry_count == 1 - assert trace.data.latency > 0 + assert trace.op == "Sentry.Integrations.Phoenix.ObanTest.TestWorker process" + assert trace.data["oban.job.job_id"] + assert trace.data["messaging.destination"] == "default" + assert trace.data["oban.job.attempt"] == 1 - assert [] = transaction.spans + assert [_span] = transaction.spans end end diff --git a/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs b/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs index b9ad2a35..7a61e524 100644 --- a/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs +++ b/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs @@ -20,8 +20,8 @@ defmodule PhoenixApp.RepoTest do assert [transaction] = transactions - assert transaction.transaction_info == %{source: "component"} - assert transaction.contexts.trace.op == "db.sql.ecto" + assert transaction.transaction_info == %{source: :custom} + assert transaction.contexts.trace.op == "db" assert String.starts_with?(transaction.contexts.trace.description, "SELECT") assert transaction.contexts.trace.data["db.system"] == :sqlite end diff --git a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs index 5d90a0fc..49e214da 100644 --- a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs +++ b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs @@ -10,6 +10,8 @@ defmodule Sentry.Integrations.Phoenix.TransactionTest do end test "GET /transaction", %{conn: conn} do + # TODO: Wrap this in a transaction that the web server usually + # would wrap it in. get(conn, ~p"/transaction") transactions = Sentry.Test.pop_sentry_transactions() @@ -18,24 +20,19 @@ defmodule Sentry.Integrations.Phoenix.TransactionTest do assert [transaction] = transactions - assert transaction.transaction == "Elixir.PhoenixAppWeb.PageController#transaction" - assert transaction.transaction_info == %{source: "view"} + assert transaction.transaction == "test_span" + assert transaction.transaction_info == %{source: :custom} trace = transaction.contexts.trace - assert trace.origin == "opentelemetry_phoenix" - assert trace.op == "http.server" - assert trace.data == %{"http.response.status_code" => 200} - assert trace.status == "ok" - - assert transaction.request.env == %{"SERVER_NAME" => "www.example.com", "SERVER_PORT" => 80} - assert transaction.request.url == "http://www.example.com/transaction" - assert transaction.request.method == "GET" + assert trace.origin == "phoenix_app" + assert trace.op == "test_span" + assert trace.data == %{} assert [span] = transaction.spans assert span.op == "test_span" assert span.trace_id == trace.trace_id - assert span.parent_span_id == trace.span_id + refute span.parent_span_id end test "GET /users", %{conn: conn} do @@ -43,29 +40,37 @@ defmodule Sentry.Integrations.Phoenix.TransactionTest do transactions = Sentry.Test.pop_sentry_transactions() - assert length(transactions) == 1 + assert length(transactions) == 2 - assert [transaction] = transactions + assert [mount_transaction, handle_params_transaction] = transactions - assert transaction.transaction == "Elixir.Phoenix.LiveView.Plug#index" - assert transaction.transaction_info == %{source: "view"} + assert mount_transaction.transaction == "PhoenixAppWeb.UserLive.Index.mount" + assert mount_transaction.transaction_info == %{source: :custom} - trace = transaction.contexts.trace + trace = mount_transaction.contexts.trace assert trace.origin == "opentelemetry_phoenix" - assert trace.op == "http.server" - assert trace.data == %{"http.response.status_code" => 200} - assert trace.status == "ok" - - assert transaction.request.env == %{"SERVER_NAME" => "www.example.com", "SERVER_PORT" => 80} - assert transaction.request.url == "http://www.example.com/users" - assert transaction.request.method == "GET" + assert trace.op == "PhoenixAppWeb.UserLive.Index.mount" + assert trace.data == %{} - assert [span_mount, span_handle_params] = transaction.spans + assert [span_mount, span_ecto] = mount_transaction.spans - assert span_mount.op == "http.server.live" + assert span_mount.op == "PhoenixAppWeb.UserLive.Index.mount" assert span_mount.description == "PhoenixAppWeb.UserLive.Index.mount" - assert span_handle_params.op == "http.server.live" + assert span_ecto.op == "db" + assert span_ecto.description == "SELECT u0.\"id\", u0.\"name\", u0.\"age\", u0.\"inserted_at\", u0.\"updated_at\" FROM \"users\" AS u0" + + assert handle_params_transaction.transaction == "PhoenixAppWeb.UserLive.Index.handle_params" + assert handle_params_transaction.transaction_info == %{source: :custom} + + trace = handle_params_transaction.contexts.trace + assert trace.origin == "opentelemetry_phoenix" + assert trace.op == "PhoenixAppWeb.UserLive.Index.handle_params" + assert trace.data == %{} + + assert [span_handle_params] = handle_params_transaction.spans + + assert span_handle_params.op == "PhoenixAppWeb.UserLive.Index.handle_params" assert span_handle_params.description == "PhoenixAppWeb.UserLive.Index.handle_params" end end diff --git a/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs b/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs index c9d00f17..d2414b58 100644 --- a/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs +++ b/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs @@ -60,17 +60,17 @@ defmodule PhoenixAppWeb.UserLiveTest do end) assert transaction_save.transaction == "PhoenixAppWeb.UserLive.Index.handle_event#save" - assert transaction_save.transaction_info.source == "view" - assert transaction_save.contexts.trace.op == "http.server.live" + assert transaction_save.transaction_info.source == :custom + assert transaction_save.contexts.trace.op == "PhoenixAppWeb.UserLive.Index.handle_event#save" assert transaction_save.contexts.trace.origin == "opentelemetry_phoenix" - assert length(transaction_save.spans) == 1 - span = List.first(transaction_save.spans) - assert span.op == "db.sql.ecto" - assert span.description =~ "INSERT INTO \"users\"" - assert span.data["db.system"] == :sqlite - assert span.data["db.type"] == :sql - assert span.origin == "opentelemetry_ecto" + assert length(transaction_save.spans) == 2 + assert [_span_1, span_2] = transaction_save.spans + assert span_2.op == "db" + assert span_2.description =~ "INSERT INTO \"users\"" + assert span_2.data["db.system"] == :sqlite + assert span_2.data["db.type"] == :sql + assert span_2.origin == "opentelemetry_ecto" end test "updates user in listing", %{conn: conn, user: user} do