From 91c29a8807f941bebc1791044df355b83663823f Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 31 Dec 2025 21:10:25 -0500 Subject: [PATCH] feat: add W3C Trace Context propagation middleware for OpenTelemetry Signed-off-by: Yordis Prieto --- guides/explanations/fork-differences.md | 25 +++ .../middleware/trace_context_propagator.ex | 66 ++++++++ mix.exs | 15 +- mix.lock | 14 ++ .../trace_context_propagator_test.exs | 151 ++++++++++++++++++ 5 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 lib/commanded/middleware/trace_context_propagator.ex create mode 100644 test/opentelemetry/trace_context_propagator_test.exs diff --git a/guides/explanations/fork-differences.md b/guides/explanations/fork-differences.md index dd8a4819..6404d6f1 100644 --- a/guides/explanations/fork-differences.md +++ b/guides/explanations/fork-differences.md @@ -146,3 +146,28 @@ defmodule MyApp.Repo.Migrations.UpgradeProjectionVersionsTimestamps do end end ``` + +### **W3C Trace Context Propagation Middleware** +[PR #38](https://github.com/straw-hat-team/commanded/pull/38) + +**Changes:** +- Added `Commanded.Middleware.TraceContextPropagator` middleware for propagating OpenTelemetry trace context +- Captures current span context and stores it in event metadata using W3C Trace Context standard +- Added `opentelemetry_api` as optional dependency + +**Usage:** +```elixir +defmodule MyApp.Router do + use Commanded.Commands.Router + + middleware Commanded.Middleware.TraceContextPropagator + + # ... your command routes +end +``` + +**Benefits:** +- Enables distributed tracing correlation between command dispatch and event handlers +- Uses standard W3C `traceparent` and `tracestate` headers stored in event metadata +- Event handlers can extract trace context to create span links or parent-child relationships +- Non-invasive - only adds metadata when a span is active diff --git a/lib/commanded/middleware/trace_context_propagator.ex b/lib/commanded/middleware/trace_context_propagator.ex new file mode 100644 index 00000000..c5403fa9 --- /dev/null +++ b/lib/commanded/middleware/trace_context_propagator.ex @@ -0,0 +1,66 @@ +if Code.ensure_loaded?(:otel_propagator_text_map) do + defmodule Commanded.Middleware.TraceContextPropagator do + @moduledoc """ + A middleware for propagating W3C Trace Context to Event Handlers. + + This middleware captures the current span context and stores it in event metadata + following the [W3C Trace Context](https://www.w3.org/TR/trace-context-2/) specification, + allowing event handlers to create proper span links or parent-child relationships. + + ## W3C Trace Context + + The middleware stores trace context using the standard W3C header names: + + * `traceparent` - Contains version, trace-id, parent-id (span-id), and trace-flags + * `tracestate` - Vendor-specific key-value pairs (optional, only set if present) + + Example traceparent: `00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01` + + ## Dependencies + + This middleware requires `opentelemetry_api` to be installed: + + {:opentelemetry_api, "~> 1.0"} + + ## Usage + + ```elixir + defmodule BankRouter do + use Commanded.Commands.Router + + middleware Commanded.Middleware.TraceContextPropagator + + dispatch [OpenAccount, DepositMoney], + to: BankAccount, + identity: :account_number + end + ``` + """ + + @behaviour Commanded.Middleware + + alias Commanded.Middleware.Pipeline + + @doc false + def before_dispatch(%Pipeline{} = pipeline) do + case :otel_propagator_text_map.inject([]) do + [] -> + pipeline + + headers -> + pipeline + |> maybe_assign("traceparent", List.keyfind(headers, "traceparent", 0)) + |> maybe_assign("tracestate", List.keyfind(headers, "tracestate", 0)) + end + end + + defp maybe_assign(pipeline, _key, nil), do: pipeline + + defp maybe_assign(pipeline, key, {_, value}), + do: Pipeline.assign_metadata(pipeline, key, value) + + def after_dispatch(pipeline), do: pipeline + + def after_failure(pipeline), do: pipeline + end +end diff --git a/mix.exs b/mix.exs index 0e17aac8..f95048d4 100644 --- a/mix.exs +++ b/mix.exs @@ -77,6 +77,7 @@ defmodule Commanded.Mixfile do {:eventstore, "~> 1.4", optional: true}, {:ecto, "~> 3.11", optional: true}, {:ecto_sql, "~> 3.11", optional: true}, + {:opentelemetry_api, "~> 1.0", optional: true}, # Build and test tools {:benchfella, "~> 0.3", only: :bench}, @@ -85,7 +86,9 @@ defmodule Commanded.Mixfile do {:ex_doc, ">= 0.0.0", only: :dev}, {:local_cluster, "~> 2.1", only: :test, runtime: false}, {:mix_test_watch, "~> 1.1", only: :dev}, - {:mox, "~> 1.0", only: [:bench, :test]} + {:mox, "~> 1.0", only: [:bench, :test]}, + {:opentelemetry, "~> 1.0", only: :test}, + {:opentelemetry_exporter, "~> 1.0", only: :test} ] end @@ -234,7 +237,15 @@ defmodule Commanded.Mixfile do defp dialyzer do [ ignore_warnings: ".dialyzer_ignore.exs", - plt_add_apps: [:ex_unit, :jason, :mix, :phoenix_pubsub, :ecto, :ecto_sql], + plt_add_apps: [ + :ex_unit, + :jason, + :mix, + :phoenix_pubsub, + :ecto, + :ecto_sql, + :opentelemetry_api + ], plt_add_deps: :app_tree, plt_file: {:no_warn, "priv/plts/commanded.plt"} ] diff --git a/mix.lock b/mix.lock index 2652c4c4..17fec0f2 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,11 @@ %{ + "acceptor_pool": {:hex, :acceptor_pool, "1.0.1", "d88c2e8a0be9216cf513fbcd3e5a4beb36bee3ff4168e85d6152c6f899359cdb", [:rebar3], [], "hexpm", "f172f3d74513e8edd445c257d596fc84dbdd56d2c6fa287434269648ae5a421e"}, "backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"}, "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm", "23f27cbc482cbac03fc8926441eb60a5e111759c17642bac005c3225f5eb809d"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, @@ -16,6 +19,9 @@ "fsm": {:hex, :fsm, "0.3.1", "087aa9b02779a84320dc7a2d8464452b5308e29877921b2bde81cdba32a12390", [:mix], [], "hexpm", "fbf0d53f89e9082b326b0b5828b94b4c549ff9d1452bbfd00b4d1ac082208e96"}, "gen_stage": {:hex, :gen_stage, "1.3.0", "d3e102a781e30de7bc14f5bacf31c4563f5390733d6d811dd020421803cae3eb", [:mix], [], "hexpm", "02b4263f8937a5039db34637879c3332dd5eb909b463493a0e7ab996dfcf19fa"}, "global_flags": {:hex, :global_flags, "1.0.0", "ee6b864979a1fb38d1fbc67838565644baf632212bce864adca21042df036433", [:rebar3], [], "hexpm", "85d944cecd0f8f96b20ce70b5b16ebccedfcd25e744376b131e89ce61ba93176"}, + "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"}, + "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "local_cluster": {:hex, :local_cluster, "2.1.0", "1c847d69a927ef5a62db13236f93146e8a42377a9c9a5bb4cac3372cba69d683", [:mix], [{:global_flags, "~> 1.0", [hex: :global_flags, repo: "hexpm", optional: false]}], "hexpm", "dc1c3abb6fef00198dd53c855b39ea80c55b3a8059d8d9f17d50da46b1e3b858"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -23,11 +29,19 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "mix_test_watch": {:hex, :mix_test_watch, "1.3.0", "2ffc9f72b0d1f4ecf0ce97b044e0e3c607c3b4dc21d6228365e8bc7c2856dc77", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f9e5edca976857ffac78632e635750d158df14ee2d6185a15013844af7570ffe"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "opentelemetry": {:hex, :opentelemetry, "1.7.0", "20d0f12d3d1c398d3670fd44fd1a7c495dd748ab3e5b692a7906662e2fb1a38a", [:rebar3], [{:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "a9173b058c4549bf824cbc2f1d2fa2adc5cdedc22aa3f0f826951187bbd53131"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.5.0", "1a676f3e3340cab81c763e939a42e11a70c22863f645aa06aafefc689b5550cf", [:mix, :rebar3], [], "hexpm", "f53ec8a1337ae4a487d43ac89da4bd3a3c99ddf576655d071deed8b56a2d5dda"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.10.0", "972e142392dbfa679ec959914664adefea38399e4f56ceba5c473e1cabdbad79", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.7.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "33a116ed7304cb91783f779dec02478f887c87988077bfd72840f760b8d4b952"}, + "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"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.31.0", "9a910b54d8cb96cc810cabf4c0129f21360f82022b20180849f1442a25ccbb04", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "9d2b41b128d5507bd8ad93e1a998e06d0ab2f9a772af343f4c00bf76c6be1532"}, "uniq": {:hex, :uniq, "0.6.1", "369660ecbc19051be526df3aa85dc393af5f61f45209bce2fa6d7adb051ae03c", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "6426c34d677054b3056947125b22e0daafd10367b85f349e24ac60f44effb916"}, } diff --git a/test/opentelemetry/trace_context_propagator_test.exs b/test/opentelemetry/trace_context_propagator_test.exs new file mode 100644 index 00000000..850fa377 --- /dev/null +++ b/test/opentelemetry/trace_context_propagator_test.exs @@ -0,0 +1,151 @@ +defmodule Commanded.Middleware.TraceContextPropagatorTest do + use ExUnit.Case, async: true + + alias Commanded.Middleware.Commands.Fail + alias Commanded.Middleware.Pipeline + alias Commanded.Middleware.TraceContextPropagator + + require OpenTelemetry.Tracer, as: Tracer + + @trace_id "0af7651916cd43dd8448eb211c80319c" + @span_id "b7ad6b7169203331" + @traceparent "00-#{@trace_id}-#{@span_id}-01" + @tracestate "vendor1=value1,vendor2=value2" + + describe "before_dispatch/1" do + test "captures traceparent when span context is active" do + set_trace_context(@traceparent) + + pipeline = %Pipeline{command: %Fail{}, metadata: %{}} + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.metadata == %{"traceparent" => @traceparent} + end + + test "does not modify metadata when no span is active" do + pipeline = %Pipeline{command: %Fail{}, metadata: %{}} + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.metadata == %{} + end + + test "preserves existing metadata" do + set_trace_context(@traceparent) + + pipeline = %Pipeline{ + command: %Fail{}, + metadata: %{"user_id" => "123", "tenant" => "acme"} + } + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.metadata == %{ + "user_id" => "123", + "tenant" => "acme", + "traceparent" => @traceparent + } + end + + test "does not set tracestate when span has no tracestate" do + set_trace_context(@traceparent) + + pipeline = %Pipeline{command: %Fail{}, metadata: %{}} + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.metadata == %{"traceparent" => @traceparent} + end + + test "captures tracestate when present in span context" do + set_trace_context(@traceparent, @tracestate) + + pipeline = %Pipeline{command: %Fail{}, metadata: %{}} + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.metadata == %{ + "traceparent" => @traceparent, + "tracestate" => @tracestate + } + end + + test "child span inherits trace_id and tracestate from parent" do + set_trace_context(@traceparent, "vendor=parentvalue") + + Tracer.with_span "child.span" do + pipeline = %Pipeline{command: %Fail{}, metadata: %{}} + + result = TraceContextPropagator.before_dispatch(pipeline) + + # Extract child's span_id (dynamic) + "00-" <> @trace_id <> "-" <> rest = result.metadata["traceparent"] + [child_span_id, "01"] = String.split(rest, "-") + + assert result.metadata == %{ + "traceparent" => "00-#{@trace_id}-#{child_span_id}-01", + "tracestate" => "vendor=parentvalue" + } + + # Span ID should be different from parent's + refute child_span_id == @span_id + end + end + + test "does not modify pipeline command" do + set_trace_context(@traceparent) + + command = %Fail{} + pipeline = %Pipeline{command: command, metadata: %{}} + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.command == command + assert result.metadata == %{"traceparent" => @traceparent} + end + + test "does not modify pipeline assigns" do + set_trace_context(@traceparent) + + pipeline = %Pipeline{ + command: %Fail{}, + metadata: %{}, + assigns: %{existing: "value"} + } + + result = TraceContextPropagator.before_dispatch(pipeline) + + assert result.assigns == %{existing: "value"} + assert result.metadata == %{"traceparent" => @traceparent} + end + end + + describe "after_dispatch/1" do + test "returns pipeline unchanged" do + pipeline = %Pipeline{command: %Fail{}, metadata: %{"test" => "value"}} + + assert TraceContextPropagator.after_dispatch(pipeline) == pipeline + end + end + + describe "after_failure/1" do + test "returns pipeline unchanged" do + pipeline = %Pipeline{command: %Fail{}, metadata: %{"test" => "value"}} + + assert TraceContextPropagator.after_failure(pipeline) == pipeline + end + end + + defp set_trace_context(traceparent) when is_binary(traceparent) do + :otel_propagator_text_map.extract([{"traceparent", traceparent}]) + end + + defp set_trace_context(traceparent, tracestate) + when is_binary(traceparent) and is_binary(tracestate) do + :otel_propagator_text_map.extract([ + {"traceparent", traceparent}, + {"tracestate", tracestate} + ]) + end +end