Skip to content

Commit

Permalink
WIP on sampling. FORK ME if you need to experiment.
Browse files Browse the repository at this point in the history
  • Loading branch information
garthk committed May 20, 2020
1 parent b860ac5 commit 128cc8f
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 20 deletions.
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,68 @@ config :opentelemetry,
```

`processors` specifies `ot_batch_processor`, which specifies `exporter`, a 2-tuple of the
exporter's module name and options to be supplied to its `init/1`. Our exporter takes a list of
`t:OpenTelemetry.Honeycomb.Config.config_opt/0` as its options.
exporter's module name and options to be supplied to its `init/1`.

Our exporter takes a keyword list of `t:OpenTelemetry.Honeycomb.Config.config_opt/0` as its
options, with keys:

* `api_endpoint`: the API endpoint
* `attribute_map`: a map to control dataset attributes used for span properties
(see `OpenTelemetry.Honeycomb.Config.AttributeMap`)
* `dataset`: the Honeycomb dataset name
* `http_module`: the HTTP back end module (see `OpenTelemetry.Honeycomb.Http`)
* `http_options`: options to pass to the HTTP back end (see `OpenTelemetry.Honeycomb.Http`)
* `samplerate_key`: the attribute key smuggling your sample rate to Honeycomb (see below)
* `json_module`: the HTTP back end module (see `OpenTelemetry.Honeycomb.Json`)
* `write_key`: the write key (see below)

`samplerate_key` provides interim support for reporting your [dynamic sampling][hcds] decisions
to Honeycomb, in advance of any support from the OpenTelemetry specification; see
[Sampling](#sampling) below.

If the `write_key` is absent or `nil`, the exporter replaces your `http_module` with
`OpenTelemetry.Honeycomb.Http.WriteKeyMissingBackend` to prevent spamming Honeycomb with
unauthenticated requests.

[hcds]: https://docs.honeycomb.io/working-with-your-data/best-practices/sampling/#dynamic-sampling
[MUST]: https://tools.ietf.org/html/rfc2119#section-1
[MAY]: https://tools.ietf.org/html/rfc2119#section-5

## Sampling

To sampling your events before sending them, configure `samplerate_key` to some key that won't
collide with the rest of your data, _eg._ `"...samplerate"` or `"hc_sample"`:

```elixir
config :opentelemetry,
processors: [
ot_batch_processor: %{
scheduled_delay_ms: 1,
exporter:
{OpenTelemetry.Honeycomb.Exporter,
samplerate_key: "...samplerate",
write_key: "HONEYCOMB_WRITEKEY"}
}
]
```

Set that attribute on your spans to whatever postitive integer seems appropriate at the time:

```elixir
OpenTelemetry.Span.set_attribute("...samplerate", 42)
```

The attribute value [MUST] be a positive integer. You [MAY] skip setting it, in which case we'll
assume you wanted the span _ie._ the sample rate was 1.

The exporter will:

* Pop the sample rate key out of the attribute map_ so it can't be confused for a measurement

* Send it to Honeycomb, who'll then use it to compensate their `COUNT` and other visualised data.

Please treat this as an interim API. We trust the OpenTelemetry specification will evolve to cover
this use case, after which you'll be able to get this result without an implementation kludge.

<!-- CDOC !-->

Expand Down
5 changes: 4 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ config :opentelemetry,
scheduled_delay_ms: 1,
exporter:
{OpenTelemetry.Honeycomb.Exporter,
http_module: MockedHttpBackend, http_options: [], write_key: "HONEYCOMB_WRITEKEY"}
http_module: MockedHttpBackend,
http_options: [],
samplerate_key: "...samplerate",
write_key: "HONEYCOMB_WRITEKEY"}
}
]
1 change: 1 addition & 0 deletions lib/open_telemetry/honeycomb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule OpenTelemetry.Honeycomb do
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
|> (&Regex.replace(~R{\(\#\K(?=[a-z][a-z0-9-]+\))}, &1, "module-")).()

@doc "Get the version string for the OpenTelemetry Honeycomb exporter."
def version do
Expand Down
2 changes: 2 additions & 0 deletions lib/open_telemetry/honeycomb/attributes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule OpenTelemetry.Honeycomb.Attributes do
|> File.read!()
|> String.split("<!-- ADOC !-->")
|> Enum.fetch!(1)
|> (&Regex.replace(~R{\(\#\K(?=[a-z][a-z0-9-]+\))}, &1, "module-")).()
}
"""

Expand Down Expand Up @@ -111,6 +112,7 @@ defmodule OpenTelemetry.Honeycomb.Attributes do
|> File.read!()
|> String.split("<!-- TRIMDOC !-->")
|> Enum.fetch!(1)
|> (&Regex.replace(~R{\(\#\K(?=[a-z][a-z0-9-]+\))}, &1, "module-")).()
}
"""
def trim_long_strings({k, <<_::binary-size(@hc_value_limit)>> = v}), do: {k, v}
Expand Down
16 changes: 3 additions & 13 deletions lib/open_telemetry/honeycomb/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule OpenTelemetry.Honeycomb.Config do
|> File.read!()
|> String.split("<!-- CDOC !-->")
|> Enum.fetch!(1)
|> (&Regex.replace(~R{\(\#\K(?=[a-z][a-z0-9-]+\))}, &1, "module-")).()
}
"""

Expand All @@ -47,25 +48,14 @@ defmodule OpenTelemetry.Honeycomb.Config do
alias OpenTelemetry.Honeycomb.Json

@typedoc """
Configuration option for the OpenTelemetry Honeycomb exporter, giving:
* `api_endpoint`: the API endpoint
* `attribute_map`: a map to control dataset attributes used for span properties (see below)
* `dataset`: the Honeycomb dataset name
* `http_module`: the HTTP back end module (see `Http`)
* `http_options`: options to pass to the HTTP back end (see `Http`)
* `json_module`: the HTTP back end module (see `Json`)
* `write_key`: the write key
If the `write_key` is absent or `nil`, the exporter replaces your `http_module` with
`OpenTelemetry.Honeycomb.Http.WriteKeyMissingBackend` to prevent spamming Honeycomb with
unauthenticated requests.
Configuration option for the OpenTelemetry Honeycomb exporter.
"""
@type config_opt ::
{:api_endpoint, String.t()}
| {:attribute_map, AttributeMap.t()}
| {:dataset, String.t()}
| {:write_key, String.t() | nil}
| {:samplerate_key, String.t() | nil}
| Http.config_opt()
| Json.config_opt()

Expand Down
13 changes: 10 additions & 3 deletions lib/open_telemetry/honeycomb/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ defmodule OpenTelemetry.Honeycomb.Event do
@spec from_ot_span(
:opentelemetry.span(),
resource_attributes :: OpenTelemetry.attributes(),
attribute_map :: AttributeMap.t()
attribute_map :: AttributeMap.t(),
samplerate_key :: nil | String.t()
) :: [t()]
def from_ot_span(ot_span, resource_attributes, attribute_map) do
def from_ot_span(ot_span, resource_attributes, attribute_map, samplerate_key \\ nil) do
span = Span.from(ot_span)

data =
Expand All @@ -77,9 +78,15 @@ defmodule OpenTelemetry.Honeycomb.Event do
|> DateTime.from_unix!(:microsecond)
|> DateTime.to_iso8601()

[%__MODULE__{time: time, data: data}]
{samplerate, data} = pop_sample_rate(data, samplerate_key)
[%__MODULE__{time: time, data: data, samplerate: samplerate}]
end

@spec pop_sample_rate(event_data(), nil | String.t()) :: {pos_integer(), event_data()}
defp pop_sample_rate(data, samplerate_key)
defp pop_sample_rate(data, nil), do: {1, data}
defp pop_sample_rate(data, samplerate_key), do: Map.pop(data, samplerate_key, 1)

# span attribute extractors
@spec extracted_attributes(Span.t(), AttributeMap.t()) :: OpenTelemetry.attributes()
defp extracted_attributes(%Span{} = span, attribute_map) do
Expand Down
18 changes: 17 additions & 1 deletion lib/open_telemetry/honeycomb/exporter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,32 @@ defmodule OpenTelemetry.Honeycomb.Exporter do
defp export_loaded(spans, resource, config) do
resource_attributes = :ot_resource.attributes(resource) |> Attributes.sort()
attribute_map = config[:attribute_map]
cook = fn ot_span -> Event.from_ot_span(ot_span, resource_attributes, attribute_map) end

cook = fn ot_span ->
Event.from_ot_span(
ot_span,
resource_attributes,
attribute_map,
Keyword.get(config, :samplerate_key)
)
end

spans
|> Enum.flat_map(cook)
|> Enum.filter(&survived_sampling?/1)
|> Enum.map(&Json.encode_to_iodata!(config, &1))
|> chunk_events()
|> Enum.map(&send_batch(&1, config))
|> Enum.find_value(:ok, &find_hc_batch_errors/1)
end

defp survived_sampling?(%Event{samplerate: 1}), do: true

defp survived_sampling?(%Event{samplerate: n}) when is_integer(n) and n > 0,
do: :rand.uniform(n) == 1

defp survived_sampling?(_), do: false

defp find_hc_batch_errors(:ok), do: false
defp find_hc_batch_errors(error), do: error

Expand Down
59 changes: 59 additions & 0 deletions test/opentelemetry/honeycomb/sampling_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule OpenTelemetry.Honeycomb.SamplingTest do
use ExUnit.Case, async: false

require OpenTelemetry.Tracer
require OpenTelemetry.Span

import Mox, only: [set_mox_from_context: 1, verify_on_exit!: 1]

setup :set_mox_from_context
setup :verify_on_exit!

@attempts 100

test "#{@attempts} spans with a sample rate of 4" do
test_pid = self()

samplerate_key =
:opentelemetry
|> Application.get_all_env()
|> get_in([:processors, :ot_batch_processor, :exporter, Access.elem(1), :samplerate_key])

Mox.expect(MockedHttpBackend, :request, fn :post, _, _, body, _ ->
try do
assert events = body |> IO.iodata_to_binary() |> Poison.decode!()
assert length(events) > 1, "opportunity for VERY rare build failure"

replies =
for %{"data" => data, "samplerate" => samplerate} <- events do
refute samplerate == 1, "sample rate not set"
refute samplerate_key in Map.keys(data), "samplerate_key #{samplerate_key} not popped"
%{"status" => 202}
end

send(test_pid, {:mock_result, :ok})
{:ok, 200, [], Poison.encode!(replies)}
rescue
e ->
send(test_pid, {:mock_result, :error, e})
{:ok, 400, [], "[]"}
end
end)

for n <- 1..@attempts do
OpenTelemetry.Tracer.with_span "some-span" do
OpenTelemetry.Span.set_attribute("n", n)
OpenTelemetry.Span.set_attribute(samplerate_key, 4)

OpenTelemetry.Span.add_events([
OpenTelemetry.event("event.name", [{"event.attr1", "event.value1"}])
])
end
end

receive do
{:mock_result, :ok} -> :ok
{:mock_result, :error, e} -> raise e
end
end
end

0 comments on commit 128cc8f

Please sign in to comment.