Skip to content

Commit

Permalink
Dimensional metric reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
tpitale committed Aug 4, 2022
1 parent f109ced commit 95bef69
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 3 deletions.
11 changes: 11 additions & 0 deletions lib/new_relic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,17 @@ defmodule NewRelic do
defdelegate report_custom_event(type, attributes),
to: NewRelic.Harvest.Collector.CustomEvent.Harvester

@doc """
Report a Dimensional Metric.
Valid types: `:count`, `:gauge`, and `:summary`.
```elixir
NewRelic.report_dimensional_metric(:count, "my_metric_name", 1, %{some: "attributes"})
```
"""
defdelegate report_dimensional_metric(type, name, value, attributes \\ %{}),
to: NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester

@doc """
Report a Custom metric.
Expand Down
8 changes: 8 additions & 0 deletions lib/new_relic/harvest/telemetry_sdk/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ defmodule NewRelic.Harvest.TelemetrySdk.API do
|> maybe_retry(url, payload)
end

def dimensional_metric(metrics) do
url = url(:metric)
payload = {:metrics, metrics, generate_request_id()}

request(url, payload)
|> maybe_retry(url, payload)
end

def request(url, payload) do
post(url, payload)
end
Expand Down
10 changes: 8 additions & 2 deletions lib/new_relic/harvest/telemetry_sdk/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ defmodule NewRelic.Harvest.TelemetrySdk.Config do

@default %{
logs_harvest_cycle: 5_000,
spans_harvest_cycle: 5_000
spans_harvest_cycle: 5_000,
dimensional_metrics_harvest_cycle: 5_000
}
def lookup(key) do
Application.get_env(:new_relic_agent, key, @default[key])
Expand All @@ -18,7 +19,8 @@ defmodule NewRelic.Harvest.TelemetrySdk.Config do

%{
log: "https://#{env}log-api.#{region}newrelic.com/log/v1",
trace: trace_domain(env, region)
trace: trace_domain(env, region),
metric: metric_domain(env, region)
}
end

Expand All @@ -34,4 +36,8 @@ defmodule NewRelic.Harvest.TelemetrySdk.Config do
defp trace_domain(_env, _region, infinite_tracing_host) do
"https://#{infinite_tracing_host}/trace/v1"
end

defp metric_domain(env, region) do
"https://#{env}metric-api.#{region}newrelic.com/metric/v1"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
defmodule NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester do
use GenServer

@moduledoc false

alias NewRelic.Harvest
alias NewRelic.Harvest.TelemetrySdk

@interval_ms 5_000

@valid_types [:count, :gauge, :summary]

def start_link(_) do
GenServer.start_link(__MODULE__, [])
end

def init(_) do
{:ok,
%{
start_time_ms: System.system_time(:millisecond),
metrics: []
}}
end

# API

@spec report_dimensional_metric(:count | :gauge | :summary, atom() | binary(), any, map()) ::
:ok
def report_dimensional_metric(type, name, value, attributes) when type in @valid_types do
TelemetrySdk.DimensionalMetrics.HarvestCycle
|> Harvest.HarvestCycle.current_harvester()
|> GenServer.cast({:report, %{type: type, name: name, value: value, attributes: attributes}})
end

def gather_harvest,
do:
TelemetrySdk.DimensionalMetrics.HarvestCycle
|> Harvest.HarvestCycle.current_harvester()
|> GenServer.call(:gather_harvest)

# do not accept more report messages when harvest has already been reported
def handle_cast(_late_msg, :completed), do: {:noreply, :completed}

def handle_cast({:report, metric}, state) do
# TODO: merge metrics with the same type/name/attributes?
{:noreply, %{state | metrics: [metric | state.metrics]}}
end

# do not resend metrics when harvest has already been reported
def handle_call(_late_msg, _from, :completed), do: {:reply, :completed, :completed}

def handle_call(:send_harvest, _from, state) do
send_harvest(state)
{:reply, :ok, :completed}
end

def handle_call(:gather_harvest, _from, state) do
{:reply, build_dimensional_metric_data(state.metrics, state), state}
end

# Helpers

defp send_harvest(state) do
TelemetrySdk.API.log(build_dimensional_metric_data(state.metrics, state))
log_harvest(length(state.metrics))
end

defp log_harvest(harvest_size) do
NewRelic.log(
:debug,
"Completed TelemetrySdk.DimensionalMetrics harvest - size: #{harvest_size}"
)
end

defp build_dimensional_metric_data(metrics, state) do
[
%{
metrics: metrics,
common: common(state)
}
]
end

defp common(%{start_time_ms: start_time_ms}) do
%{"timestamp" => start_time_ms, "interval.ms" => @interval_ms}
end
end
3 changes: 2 additions & 1 deletion lib/new_relic/harvest/telemetry_sdk/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ defmodule NewRelic.Harvest.TelemetrySdk.Supervisor do
def init(_) do
children = [
data_supervisor(TelemetrySdk.Logs, :logs_harvest_cycle),
data_supervisor(TelemetrySdk.Spans, :spans_harvest_cycle)
data_supervisor(TelemetrySdk.Spans, :spans_harvest_cycle),
data_supervisor(TelemetrySdk.DimensionalMetrics, :dimensional_metrics_harvest_cycle)
]

Supervisor.init(children, strategy: :one_for_all)
Expand Down
28 changes: 28 additions & 0 deletions test/dimensional_metric_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule DimensionalMetricTest do
use ExUnit.Case

test "reports dimensional metrics" do
TestHelper.restart_harvest_cycle(
NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle
)

NewRelic.report_dimensional_metric(:count, "memory.foo_baz", 100, %{cpu: 1000})
NewRelic.report_dimensional_metric(:summary, "memory.foo_bar", 50, %{cpu: 2000})

[%{common: common, metrics: metrics}] =
TestHelper.gather_harvest(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester)

assert common["interval.ms"] > 0
assert common["timestamp"] > 0

assert length(metrics) == 2
[metric1, metric2] = metrics
assert metric1.name == "memory.foo_bar"
assert metric1.type == :summary

assert metric2.name == "memory.foo_baz"
assert metric2.type == :count

TestHelper.pause_harvest_cycle(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle)
end
end
59 changes: 59 additions & 0 deletions test/telemetry_sdk/dimensional_metrics_harvester_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule TelemetrySdk.DimensionalMetricsHarvesterTest do
use ExUnit.Case

alias NewRelic.Harvest
alias NewRelic.Harvest.TelemetrySdk

test "Harvester collects dimensional metrics" do
{:ok, harvester} =
DynamicSupervisor.start_child(
TelemetrySdk.DimensionalMetrics.HarvesterSupervisor,
TelemetrySdk.DimensionalMetrics.Harvester
)

metric1 = %{}
GenServer.cast(harvester, {:report, metric1})

metrics = GenServer.call(harvester, :gather_harvest)
assert length(metrics) > 0
end

test "harvest cycle" do
Application.put_env(:new_relic_agent, :dimensional_metrics_harvest_cycle, 300)
TestHelper.restart_harvest_cycle(TelemetrySdk.DimensionalMetrics.HarvestCycle)

first = Harvest.HarvestCycle.current_harvester(TelemetrySdk.DimensionalMetrics.HarvestCycle)
Process.monitor(first)

# Wait until harvest swap
assert_receive {:DOWN, _ref, _, ^first, :shutdown}, 1000

second = Harvest.HarvestCycle.current_harvester(TelemetrySdk.DimensionalMetrics.HarvestCycle)
Process.monitor(second)

refute first == second
assert Process.alive?(second)

TestHelper.pause_harvest_cycle(TelemetrySdk.DimensionalMetrics.HarvestCycle)
Application.delete_env(:new_relic_agent, :dimensional_metrics_harvest_cycle)

# Ensure the last harvester has shut down
assert_receive {:DOWN, _ref, _, ^second, :shutdown}, 1000
end

test "Ignore late reports" do
TestHelper.restart_harvest_cycle(TelemetrySdk.DimensionalMetrics.HarvestCycle)

harvester =
TelemetrySdk.DimensionalMetrics.HarvestCycle
|> Harvest.HarvestCycle.current_harvester()

assert :ok == GenServer.call(harvester, :send_harvest)

GenServer.cast(harvester, {:report, :late_msg})

assert :completed == GenServer.call(harvester, :send_harvest)

TestHelper.pause_harvest_cycle(TelemetrySdk.DimensionalMetrics.HarvestCycle)
end
end

0 comments on commit 95bef69

Please sign in to comment.