Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregate dimensional metrics #408

Merged
merged 20 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
3 changes: 2 additions & 1 deletion lib/new_relic/harvest/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ defmodule NewRelic.Harvest.Supervisor do
Harvest.Collector.CustomEvent.HarvestCycle,
Harvest.Collector.ErrorTrace.HarvestCycle,
Harvest.TelemetrySdk.Logs.HarvestCycle,
Harvest.TelemetrySdk.Spans.HarvestCycle
Harvest.TelemetrySdk.Spans.HarvestCycle,
Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle
]

def start_link(_) do
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)
XiXiaPdx marked this conversation as resolved.
Show resolved Hide resolved
|> 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
173 changes: 173 additions & 0 deletions lib/new_relic/harvest/telemetry_sdk/dimensional_metrics/harvester.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
defmodule NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester do
use GenServer

@moduledoc false

alias NewRelic.Harvest
alias NewRelic.Harvest.TelemetrySdk

@interval_ms TelemetrySdk.Config.lookup(:dimensional_metrics_harvest_cycle)

@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
XiXiaPdx marked this conversation as resolved.
Show resolved Hide resolved
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, %{name: name, attributes: attributes} = metric}, state) do
if nil_attributes?(attributes) do
NewRelic.log(
XiXiaPdx marked this conversation as resolved.
Show resolved Hide resolved
:debug,
"Dimensional metric - #{name} - format is invalid. At least one attribute has an invalid nil value."
)

{:noreply, state}
else
{:noreply, %{state | metrics: merge_metric(metric, state.metrics)}}
end
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 nil_attributes?(attributes) do
Map.values(attributes)
|> Enum.member?(nil)
end

defp merge_metric(
%{type: :count, name: name, value: new_value, attributes: attributes} = metric,
metrics_acc
) do
case Map.get(metrics_acc, {:count, name, attributes}) do
nil ->
Map.put(metrics_acc, {:count, name, attributes}, metric)

%{type: :count, value: current_value} = current_metric ->
Map.put(metrics_acc, {:count, name, attributes}, %{
current_metric
| value: current_value + new_value
})
end
end

defp merge_metric(
%{type: :gauge, name: name, value: new_value, attributes: attributes} = metric,
metrics_acc
) do
case Map.get(metrics_acc, {:gauge, name, attributes}) do
nil ->
Map.put(metrics_acc, {:gauge, name, attributes}, metric)

%{type: :gauge} = current_metric ->
Map.put(metrics_acc, {:gauge, name, attributes}, %{current_metric | value: new_value})
end
end

defp merge_metric(
%{type: :summary, name: name, value: new_value, attributes: attributes},
metrics_acc
) do
case Map.get(metrics_acc, {:summary, name, attributes}) do
nil ->
new_summary = %{
type: :summary,
name: name,
value: %{
count: 1,
min: new_value,
max: new_value,
sum: new_value
},
attributes: attributes
}

Map.put(metrics_acc, {:summary, name, attributes}, new_summary)

%{type: :summary} = current_metric ->
Map.put(
metrics_acc,
{:summary, name, attributes},
%{current_metric | value: update_summary_value_map(current_metric, new_value)}
)
end
end

defp update_summary_value_map(
%{type: :summary, value: value_map},
new_value
) do
updated_sum_count = %{value_map | sum: value_map.sum + new_value, count: value_map.count + 1}

updated_min =
if new_value < value_map.min,
do: %{updated_sum_count | min: new_value},
else: updated_sum_count

if new_value > value_map.max, do: %{updated_min | max: new_value}, else: updated_min
end

defp send_harvest(state) do
metrics = Map.values(state.metrics)
TelemetrySdk.API.dimensional_metric(build_dimensional_metric_data(metrics, state))
log_harvest(length(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}
XiXiaPdx marked this conversation as resolved.
Show resolved Hide resolved
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
125 changes: 125 additions & 0 deletions test/dimensional_metric_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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_map}] =
TestHelper.gather_harvest(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester)

metrics = Map.values(metrics_map)
assert common["interval.ms"] > 0
assert common["timestamp"] > 0

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

assert metric2.name == "memory.foo_bar"
assert metric2.type == :summary

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

test "gauge dimensional metric is updated" do
TestHelper.restart_harvest_cycle(
NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle
)

NewRelic.report_dimensional_metric(:gauge, "mem_percent.foo_baz", 10, %{cpu: 1000})
NewRelic.report_dimensional_metric(:gauge, "mem_percent.foo_baz", 40, %{cpu: 1000})
NewRelic.report_dimensional_metric(:gauge, "mem_percent.foo_baz", 90, %{cpu: 1000})

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

metrics = Map.values(metrics_map)

assert length(metrics) == 1
[metric] = metrics
assert metric.name == "mem_percent.foo_baz"
assert metric.type == :gauge
assert metric.value == 90

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

test "count dimensional metric is updated" do
TestHelper.restart_harvest_cycle(
NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle
)

NewRelic.report_dimensional_metric(:count, "OOM", 1, %{cpu: 1000})
NewRelic.report_dimensional_metric(:count, "OOM", 1, %{cpu: 1000})
NewRelic.report_dimensional_metric(:count, "OOM", 2, %{cpu: 1000})

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

metrics = Map.values(metrics_map)

assert length(metrics) == 1
[metric] = metrics
assert metric.name == "OOM"
assert metric.type == :count
assert metric.value == 4

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

test "summary dimensional metric is updated" do
TestHelper.restart_harvest_cycle(
NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle
)

NewRelic.report_dimensional_metric(:summary, "duration", 40.5, %{cpu: 1000})
NewRelic.report_dimensional_metric(:summary, "duration", 20.5, %{cpu: 1000})
NewRelic.report_dimensional_metric(:summary, "duration", 9.5, %{cpu: 1000})
NewRelic.report_dimensional_metric(:summary, "duration", 55.5, %{cpu: 1000})

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

metrics = Map.values(metrics_map)

assert length(metrics) == 1
[metric] = metrics
assert metric.name == "duration"
assert metric.type == :summary
assert metric.value.sum == 126
assert metric.value.min == 9.5
assert metric.value.max == 55.5
assert metric.value.count == 4

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

test "metric with nil attribute values are rejected" do
TestHelper.restart_harvest_cycle(
NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle
)

NewRelic.report_dimensional_metric(:count, "OOM", 1, %{cpu: 1000, oops: nil})
NewRelic.report_dimensional_metric(:count, "Hits", 10, %{cpu: 1000})
NewRelic.report_dimensional_metric(:count, "OOM", 2, %{cpu: nil})

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

metrics = Map.values(metrics_map)

assert length(metrics) == 1
[metric] = metrics
assert metric.name == "Hits"
assert metric.type == :count
assert metric.value == 10

TestHelper.pause_harvest_cycle(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle)
end
end
Loading