if Code.ensure_loaded?(:opentelemetry) do
defmodule GitHub.Plugin.OpenTelemetry do
@moduledoc """
OpenTelemetry bindings for all operation requests
This module provides an easy way to report GitHub operations to OpenTelemetry. The library
has built-in support for Erlang telemetry in the default client. This module attaches to the
available telemetry events and manages OpenTelemetry tracing.
## Usage
This module requires the following optional dependencies to be included in `mix.exs`:
{:opentelemetry_api, "~> 1.0"},
{:opentelemetry_semantic_conventions, "~> 0.2"}
Unlike other plugins, this does not get included in the client stack. Instead, include it in
your main `Application` module start:
def start(_type, _args) do
GitHub.Plugin.OpenTelemetry.setup()
# ...
end
"""
alias OpenTelemetry.SemanticConventions.Trace
require Trace
require OpenTelemetry.Tracer
@doc """
Initialize Erlang telemetry handlers to produce OpenTelemetry traces
"""
@spec setup(keyword) :: :ok
def setup(_opts \\ []) do
:telemetry.attach(
{__MODULE__, :request_stop},
[:oapi_github, :request, :stop],
&__MODULE__.handle_request_stop/4,
%{}
)
:telemetry.attach(
{__MODULE__, :request_exception},
[:oapi_github, :request, :exception],
&__MODULE__.handle_request_exception/4,
%{}
)
end
@doc false
def handle_request_stop(_event, measurements, metadata, _config) do
%{duration: duration, monotonic_time: end_time} = measurements
%{
info: %{call: {call_module, call_function}},
request_method: request_method,
request_server: request_server,
request_url: request_url,
response_code: response_code
} = metadata
uri = Path.join(request_server, request_url) |> URI.parse()
start_time = end_time - duration
attributes = %{
Trace.code_namespace() => call_module,
Trace.code_function() => call_function,
Trace.http_url() => URI.to_string(uri),
Trace.http_scheme() => uri.scheme,
Trace.net_peer_name() => uri.host,
Trace.net_peer_port() => uri.port,
Trace.http_target() => uri.path,
Trace.http_method() => request_method,
Trace.http_status_code() => response_code
}
s =
OpenTelemetry.Tracer.start_span("#{inspect(call_module)}.#{call_function}", %{
start_time: start_time,
attributes: attributes,
kind: :client
})
if metadata[:error] do
OpenTelemetry.Span.set_status(
s,
OpenTelemetry.status(:error, to_string(metadata[:error]))
)
end
OpenTelemetry.Span.end_span(s)
end
@doc false
def handle_request_exception(_event, measurements, metadata, _config) do
%{duration: duration, monotonic_time: end_time} = measurements
%{
info: %{call: {call_module, call_function}},
kind: kind,
reason: reason,
request_method: request_method,
request_server: request_server,
request_url: request_url,
stacktrace: stacktrace
} = metadata
uri = Path.join(request_server, request_url) |> URI.parse()
start_time = end_time - duration
message =
case kind do
:exit -> "Process exit"
:error -> Exception.message(reason)
:throw -> "Thrown value"
end
attributes = %{
Trace.code_namespace() => call_module,
Trace.code_function() => call_function,
Trace.exception_escaped() => true,
Trace.exception_message() => message,
Trace.exception_stacktrace() => stacktrace,
Trace.http_url() => URI.to_string(uri),
Trace.http_scheme() => uri.scheme,
Trace.net_peer_name() => uri.host,
Trace.net_peer_port() => uri.port,
Trace.http_target() => uri.path,
Trace.http_method() => request_method
}
s =
OpenTelemetry.Tracer.start_span("#{inspect(call_module)}#{call_function}", %{
start_time: start_time,
attributes: attributes,
kind: :client
})
OpenTelemetry.Span.set_status(
s,
OpenTelemetry.status(:error, inspect(metadata[:reason]))
)
OpenTelemetry.Span.end_span(s)
end
end
end