Skip to content

Commit

Permalink
Merge pull request #503 from newrelic/vince/connect-other-dist-trace
Browse files Browse the repository at this point in the history
Support connecting an Other Transaction to a Distributed Trace
  • Loading branch information
tpitale authored Feb 14, 2025
2 parents e439b98 + 99fbe39 commit 33f6fcf
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 24 deletions.
48 changes: 32 additions & 16 deletions lib/new_relic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,44 +59,56 @@ defmodule NewRelic do
Start an "Other" Transaction.
This will begin monitoring the current process as an "Other" Transaction
(ie: Not a "Web" Transaction). The first argument will be considered
the "category", the second is the "name".
(ie: Not a "Web" Transaction).
The first argument will be considered the "category", the second is the "name".
The third argument is an optional map of headers that will connect this
Transaction to an existing Distributed Trace. You can provide W3C "traceparent"
and "tracestate" headers or another New Relic agent's "newrelic" header.
The Transaction will end when the process exits, or when you call
`NewRelic.stop_transaction()`
## Examples
```elixir
NewRelic.start_transaction("GenStage", "MyConsumer/EventType")
NewRelic.start_transaction("Task", "TaskName")
NewRelic.start_transaction("WebSocket", "Handler", %{"newrelic" => "..."})
```
> #### Warning {: .error}
>
> * You can't start a new transaction within an existing one. Any process
> spawned inside a transaction belongs to that transaction.
> * Do _not_ use this for processes that live a very long time, doing so
> will risk a memory leak tracking attributes in the transaction!
> will risk increased memory growth tracking attributes in the transaction!
## Notes
* Don't use this to track Web Transactions - Plug based HTTP servers
are auto-instrumented based on `telemetry` events.
* If multiple transactions are started in the same Process, you must
call `NewRelic.stop_transaction/0` to mark the end of the transaction.
call `NewRelic.stop_transaction/0` to mark the end of the Transaction.
"""
@spec start_transaction(String.t(), String.t()) :: any()
defdelegate start_transaction(category, name), to: NewRelic.OtherTransaction

@spec start_transaction(String.t(), String.t(), headers :: map) :: any()
defdelegate start_transaction(category, name, headers), to: NewRelic.OtherTransaction

@doc """
Stop an "Other" Transaction.
If multiple transactions are started in the same Process, you must
call `NewRelic.stop_transaction/0` to mark the end of the transaction.
If multiple Transactions are started in the same Process, you must
call `NewRelic.stop_transaction/0` to mark the end of the Transaction.
"""
@spec stop_transaction() :: any()
defdelegate stop_transaction(), to: NewRelic.OtherTransaction

@doc """
Record an "Other" transaction within the given block. The return value of
Record an "Other" Transaction within the given block. The return value of
the block is returned.
See `start_transaction/2` and `stop_transaction/0` for more details about
Expand Down Expand Up @@ -125,6 +137,15 @@ defmodule NewRelic do
end
end

defmacro other_transaction(category, name, headers, do: block) do
quote do
NewRelic.start_transaction(unquote(category), unquote(name), unquote(headers))
res = unquote(block)
NewRelic.stop_transaction()
res
end
end

@doc """
Call within a transaction to prevent it from reporting.
Expand Down Expand Up @@ -179,13 +200,6 @@ defmodule NewRelic do
@doc """
Store information about the type of work the current span is doing.
Options:
- `:type`
- `:generic` - Pass custom attributes
- `:http` - Pass attributes `:url`, `:method`, `:component`
- `:datastore` - Pass attributes `:statement`, `:instance`, `:address`, `:hostname`,
`:component`
## Examples
```elixir
Expand Down Expand Up @@ -226,7 +240,7 @@ defmodule NewRelic do
@doc """
You must manually instrument outgoing HTTP calls to connect them to a Distributed Trace.
The agent will automatically read request headers and detect if the request is a part
The agent will automatically read HTTP request headers and detect if the request is a part
of an incoming Distributed Trace, but outgoing requests need an extra header:
```elixir
Expand All @@ -239,6 +253,7 @@ defmodule NewRelic do
request since calling the function marks the "start" time of the request.
"""
@spec distributed_trace_headers(:http) :: [{key :: String.t(), value :: String.t()}]
@spec distributed_trace_headers(:other) :: map()
defdelegate distributed_trace_headers(type), to: NewRelic.DistributedTrace

@type name :: String.t() | {primary_name :: String.t(), secondary_name :: String.t()}
Expand Down Expand Up @@ -283,7 +298,7 @@ defmodule NewRelic do
defmodule ImportantProcess do
use GenServer
def init(:ok) do
NewRelic.sample_process
NewRelic.sample_process()
{:ok, %{}}
end
end
Expand Down Expand Up @@ -314,6 +329,7 @@ defmodule NewRelic do

@doc """
Report a Dimensional Metric.
Valid types: `:count`, `:gauge`, and `:summary`.
## Example
Expand Down
20 changes: 14 additions & 6 deletions lib/new_relic/distributed_trace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,30 @@ defmodule NewRelic.DistributedTrace do

def start(:http, headers) do
if NewRelic.Config.feature?(:distributed_tracing) do
determine_context(:http, headers)
determine_context(headers)
|> track_transaction(transport_type: "HTTP")
end

:ok
end

def start(:other, _) do
def start(:other, headers) do
if NewRelic.Config.feature?(:distributed_tracing) do
generate_new_context()
determine_context(headers)
|> track_transaction(transport_type: "Other")
end

:ok
end

defp determine_context(:http, headers) do
case accept_distributed_trace_headers(:http, headers) do
defp determine_context(headers) do
case accept_distributed_trace_headers(headers) do
%Context{} = context -> context
_ -> generate_new_context()
end
end

defp accept_distributed_trace_headers(:http, headers) do
defp accept_distributed_trace_headers(headers) do
w3c_headers(headers) || newrelic_header(headers) || :no_payload
end

Expand All @@ -54,6 +58,10 @@ defmodule NewRelic.DistributedTrace do
end
end

def distributed_trace_headers(:other) do
distributed_trace_headers(:http) |> Map.new()
end

def distributed_trace_headers(:http) do
case get_tracing_context() do
nil ->
Expand Down
4 changes: 2 additions & 2 deletions lib/new_relic/other_transaction.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule NewRelic.OtherTransaction do
@moduledoc false

def start_transaction(category, name) do
def start_transaction(category, name, headers \\ %{}) do
NewRelic.Transaction.Reporter.start_transaction(:other)
NewRelic.DistributedTrace.start(:other)
NewRelic.DistributedTrace.start(:other, headers)

NewRelic.add_attributes(
pid: inspect(self()),
Expand Down
15 changes: 15 additions & 0 deletions test/distributed_trace_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,21 @@ defmodule DistributedTraceTest do
|> Task.await()
end

test "Start an Other transaction with inbound DT headers" do
headers = %{@dt_header => generate_inbound_payload(:browser)}

Task.async(fn ->
NewRelic.start_transaction("Category", "Name", headers)

headers = NewRelic.distributed_trace_headers(:other)

context = DistributedTrace.NewRelicContext.decode(Map.get(headers, @dt_header))

assert context.trace_id == "d6b4ba0c3a712ca"
end)
|> Task.await()
end

describe "Context decoding" do
test "ignore unknown version" do
payload = %{"v" => [666]} |> NewRelic.JSON.encode!() |> Base.encode64()
Expand Down

0 comments on commit 33f6fcf

Please sign in to comment.