defmodule OpentelemetrySnowpack do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
alias OpenTelemetry.Span
require OpenTelemetry.Tracer
@tracer_id :opentelemetry_snowpack
@default_config [
span_name: "snowpack.query",
trace_query_statement: true,
trace_query_params: false,
trace_query_error: true
]
@doc """
Attaches the OpentelemetrySnowpack handler to your Snowpack events. This should be called
from your application behaviour on startup.
Example:
OpentelemetrySnowpack.setup()
"""
@spec setup(keyword) :: :ok
def setup(opts \\ []) do
config =
@default_config
|> Keyword.merge(Application.get_env(@tracer_id, :trace_options, []))
|> Keyword.merge(opts)
|> Enum.into(%{})
attach_query_start_handler(config)
attach_query_stop_handler(config)
attach_query_exception_handler(config)
:ok
end
@doc false
@spec attach_query_start_handler(map) :: :ok | {:error, :already_exists}
def attach_query_start_handler(config) do
:telemetry.attach(
{__MODULE__, :query_start},
[:snowpack, :query, :start],
&__MODULE__.handle_query_start/4,
config
)
end
@doc false
@spec attach_query_stop_handler(map) :: :ok | {:error, :already_exists}
def attach_query_stop_handler(config) do
:telemetry.attach(
{__MODULE__, :query_stop},
[:snowpack, :query, :stop],
&__MODULE__.handle_query_stop/4,
config
)
end
@doc false
@spec attach_query_exception_handler(map) :: :ok | {:error, :already_exists}
def attach_query_exception_handler(config) do
:telemetry.attach(
{__MODULE__, :query_exception},
[:snowpack, :query, :exception],
&__MODULE__.handle_query_exception/4,
config
)
end
@doc false
@spec teardown :: :ok | {:error, :not_found}
def teardown do
:telemetry.detach({__MODULE__, :query_start})
:telemetry.detach({__MODULE__, :query_stop})
:telemetry.detach({__MODULE__, :query_exception})
end
@doc false
@spec handle_query_start(any, any, any, any) :: any
def handle_query_start(_event, _measurements, metadata, config) do
attributes =
["db.type": :snowflake]
|> put_if(
config.trace_query_statement,
{:"db.statement", metadata[:query]}
)
|> put_if(config.trace_query_params, {:"db.params", inspect(metadata[:params])})
start_opts = %{kind: :client}
@tracer_id
|> OpentelemetryTelemetry.start_telemetry_span(config.span_name, metadata, start_opts)
|> Span.set_attributes(attributes)
end
@doc false
@spec handle_query_stop(any, any, any, any) :: any
def handle_query_stop(
_event,
%{duration: duration} = _measurements,
metadata,
config
) do
# ensure the correct span is current and update
ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata)
error = metadata[:error]
attributes =
put_if(
[
"db.num_rows": metadata[:num_rows],
"db.result": metadata[:result],
total_time_microseconds: System.convert_time_unit(duration, :native, :microsecond)
],
config.trace_query_error,
{:"db.error", encode_error(error)}
)
set_status(ctx, error)
Span.set_attributes(ctx, attributes)
OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata)
end
@doc false
@spec handle_query_exception(any, any, any, any) :: any
def handle_query_exception(
_event,
%{duration: duration} = _measurements,
%{error: error, stacktrace: stacktrace} = metadata,
_config
) do
# ensure the correct span is current and update
ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata)
attributes = [
total_time_microseconds: System.convert_time_unit(duration, :native, :microsecond)
]
# record exception and mark the span as errored
Span.record_exception(ctx, error, stacktrace)
set_status(ctx, error)
Span.set_attributes(ctx, attributes)
OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata)
end
# Surprisingly, there doesn't seem to be anything in the stdlib to conditionally
# put stuff in a list / keyword list.
# This snippet is approved by José himself:
# https://elixirforum.com/t/creating-list-adding-elements-on-specific-conditions/6295/4?u=learts
defp put_if(list, false, _), do: list
defp put_if(list, true, value), do: [value | list]
defp set_status(_ctx, nil), do: :ok
# coveralls-ignore-start
defp set_status(_ctx, []), do: :ok
# coveralls-ignore-stop
defp set_status(ctx, error),
do: Span.set_status(ctx, OpenTelemetry.status(:error, error_message(error)))
defp error_message(%{message: message} = _error), do: message
# coveralls-ignore-start
defp error_message(error) when is_exception(error), do: Exception.message(error)
defp error_message(error) when is_binary(error), do: error
defp error_message(_error), do: ""
# coveralls-ignore-stop
defp encode_error(error) when is_struct(error), do: error |> Map.from_struct() |> encode_error()
defp encode_error(error) when is_map(error), do: Jason.encode!(error)
defp encode_error(nil), do: nil
end