Skip to content

Commit

Permalink
rewrite focusing on telemetry (#21)
Browse files Browse the repository at this point in the history
* remove rogue inspect call

* rewrite to focus on telemetry
  • Loading branch information
the-mikedavis authored Aug 18, 2020
1 parent b627a95 commit f595ef3
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 204 deletions.
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ use Mix.Config
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"

config :hummingbird, :sender, SenderMock
10 changes: 10 additions & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"coverage_options": {
"minimum_coverage": 100,
"treat_no_relevant_lines_as_covered" : true
},
"skip_files": [
"^test",
"^deps"
]
}
17 changes: 0 additions & 17 deletions lib/helpers.ex

This file was deleted.

186 changes: 77 additions & 109 deletions lib/hummingbird.ex
Original file line number Diff line number Diff line change
@@ -1,122 +1,100 @@
defmodule Hummingbird do
@moduledoc """
Ships the conn to honeycomb.io to allow distributed tracing.
A plug for shipping events to honeycomb for tracing.
Assumes requests that come in populate two different headers:
x-b3-traceid and x-b3-spanid
Assumes that incoming requests use the b3 propagation headers.
Add it to your endpoint:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug Hummingbird
or under a branch of your router.
Add the telemetry genserver to your application:
children = [
..
Hummingbird.Telemetry,
..
]
"""

use Plug.Builder

alias Opencensus.Honeycomb.{Event, Sender}
alias Hummingbird.Helpers
alias Opencensus.Honeycomb.Event
alias Hummingbird.Impl

def init(opts) do
%{
caller: Keyword.get(opts, :caller),
service_name: Keyword.get(opts, :service_name)
}
end
@sender Application.get_env(:hummingbird, :sender, Hummingbird.Sender)

@doc """
An impure dispatching of conn information to the elixir honeycomb client
"""
def call(conn, opts) do
conn = set_trace_info(conn)
@doc false
def init(opts), do: Keyword.take(opts, [:service_name])

send_span(conn, opts)
@doc false
def call(conn, opts) do
conn |> put_private(:hummingbird, trace_info_from_conn(conn, opts))
end

@doc false
def set_trace_info(conn) do
conn
|> assign(:trace_id, determine_trace_id(conn))
|> assign(:parent_id, determine_parent_id(conn))
|> assign(:span_id, random_span_id())
|> assign(:span_start_time, Event.now())
defp trace_info_from_conn(conn, opts) do
%{
trace_id: Impl.trace_id(conn),
parent_id: Impl.parent_id(conn),
span_id: Impl.span_id(conn),
span_start: Event.now(),
sample?: Impl.sampling_state(conn),
service_name: Keyword.get(opts, :service_name),
events: []
}
end

@doc false
def send_span(conn, opts) do
[
build_generic_honeycomb_event(conn, opts)
]
|> Sender.send_batch()
def send_spans(conn) do
with {:ok, hummingbird} <- Map.fetch(conn.private, :hummingbird),
true <- hummingbird.sample? do
[build_generic_honeycomb_event(conn) | conn.private.hummingbird.events]
|> @sender.send_batch()
else
_ -> :ok
end

conn
end

@doc """
Wraps the conn in what honeycomb craves for beeeeee processing
Many things are in the conn and duplicated here. The reason is by normalizing
the output here, we can tell honeycomb to always look in the same place for
user events. They don't (afaik) have an easy ability to translate different
shapes of events on their side
For example: `booked_by:` is a different actor location than the assigns for
a conn.
Warning: this is a mix of translating on the way out to OpenCensus+Honeycomb
language. Don't know enough at this moment to disambiguate the two languages.
"""
def build_generic_honeycomb_event(conn, opts) do
@doc false
# Wraps the conn in what honeycomb craves for beeeeee processing
# Many things are in the conn and duplicated here. The reason is by normalizing
# the output here, we can tell honeycomb to always look in the same place for
# user events. They don't (afaik) have an easy ability to translate different
# shapes of events on their side

# For example: `booked_by:` is a different actor location than the assigns for
# a conn.
def build_generic_honeycomb_event(%{private: %{hummingbird: hummingbird}} = conn) do
%Event{
time: conn.assigns.span_start_time,
time: hummingbird.span_start,
data: %{
conn: Helpers.sanitize(conn),
name: opts.caller,
traceId: conn.assigns[:trace_id],
id: conn.assigns[:span_id],
parentId: conn.assigns[:parent_id],
conn: Impl.sanitize(conn),
component: "app",
name: Impl.endpoint_name(conn),
traceId: hummingbird.trace_id,
id: hummingbird.span_id,
parentId: hummingbird.parent_id,
user_id: conn.assigns[:current_user][:user_id],
route: conn.assigns[:request_path],
serviceName: opts.service_name,
durationMs: convert_time_unit(conn.assigns[:request_duration_native]),
http: http_metadata_from_conn(conn)
serviceName: hummingbird.service_name,
durationMs: Impl.convert_time_unit(conn.assigns[:request_duration_native]),
http: Impl.http_metadata_from_conn(conn)
# This is incorrect, but do not know how to programatically assign based on
# type. My intuation is we would create a different build_ for that
# application.
#
# kind: "span_event"
}
}

# |> IO.inspect(label: :event)
end

@doc """
If a span has already been created for this conn, just use that as the parent.
If not, check the headers for a span_id to hold onto and use that as your parent_id.
The latter occurs when taking in IDs from external contexts, read: commands.
If neither are set, this span should not have a parent.
"""
def determine_parent_id(conn) do
if is_nil(conn.assigns[:span_id]) do
# wasn't set previously so check header
conn
|> get_req_header("x-b3-spanid")
|> List.first()
else
conn.assigns[:span_id]
end
end

@doc """
Grabs the trace id sent over from initiating request. If nah, starts a trace
for within this application.
"""
def determine_trace_id(conn) do
if is_nil(conn.assigns[:trace_id]) do
conn
|> get_req_header("x-b3-traceid")
|> List.first() || random_trace_id()
else
# fallback to this being an internal responsibility to assign a trace id
conn.assigns[:trace_id]
end
end
def build_generic_honeycomb_event(_conn), do: nil

@doc """
Produces a random span ID.
Expand All @@ -140,26 +118,16 @@ defmodule Hummingbird do
"""
def random_trace_id, do: random_span_id(32)

defp http_metadata_from_conn(%Plug.Conn{} = conn) do
scheme = Atom.to_string(conn.scheme)

url =
%URI{
scheme: scheme,
path: conn.request_path,
port: conn.port,
host: conn.host
}
|> URI.to_string()

%{
url: url,
status_code: conn.status,
method: conn.method,
protocol: scheme
}
@doc """
Produces a list of headers for trace propagation given a conn
"""
def propagation_headers(conn) do
[
{"x-b3-traceid", Impl.trace_id(conn)},
{"x-b3-parentid", get_in(conn.private, [:hummingbird, :parent_id])},
{"x-b3-spanid", Impl.span_id(conn)},
{"x-b3-sampled", Impl.sampling_state_to_header_value(conn)}
]
|> Enum.reject(fn {_k, v} -> v |> is_nil end)
end

defp convert_time_unit(nil), do: nil
defp convert_time_unit(time) when is_integer(time), do: time / 1000
end
95 changes: 95 additions & 0 deletions lib/hummingbird/impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule Hummingbird.Impl do
@moduledoc false

import Plug.Conn, only: [get_req_header: 2]

# Internal supporting functions for managing the conn and assigning trace
# information.

# removes private information from the conn before shipping.
def sanitize(conn) do
%{
conn
| private: nil,
secret_key_base: nil
}
end

def endpoint_name(conn) do
inspect(conn.private.phoenix_endpoint) <> ".call/2"
end

def parent_id(conn) do
if get_in(conn.private, [:hummingbird, :span_id]) == nil do
# wasn't set previously so check header
conn
|> get_req_header("x-b3-spanid")
|> List.first()
else
conn.private.hummingbird.span_id
end
end

def span_id(conn) do
if get_in(conn.private, [:hummingbird, :span_id]) == nil do
Hummingbird.random_span_id()
else
conn.private.hummingbird.trace_id
end
end

def trace_id(conn) do
if get_in(conn.private, [:hummingbird, :trace_id]) == nil do
conn
|> get_req_header("x-b3-traceid")
|> List.first() || Hummingbird.random_trace_id()
else
conn.private.hummingbird.trace_id
end
end

def sampling_state(conn) do
with nil <- get_in(conn.private, [:hummingbird, :sample?]),
nil <- conn |> get_req_header("x-b3-sampled") |> List.first() do
true
else
"1" -> true
"0" -> false
prior_sampling_state -> prior_sampling_state
end
end

def http_metadata_from_conn(%Plug.Conn{} = conn) do
scheme = Atom.to_string(conn.scheme)

url =
%URI{
scheme: scheme,
path: conn.request_path,
port: conn.port,
host: conn.host
}
|> URI.to_string()

%{
url: url,
status_code: conn.status,
method: conn.method,
protocol: scheme
}
end

def convert_time_unit(nil), do: nil

def convert_time_unit(time) when is_integer(time) do
System.convert_time_unit(time, :native, :microsecond) / 1000
end

def sampling_state_to_header_value(conn) do
case sampling_state(conn) do
true -> "1"
false -> "0"
nil -> nil
end
end
end
Loading

0 comments on commit f595ef3

Please sign in to comment.