diff --git a/lib/mixpanel/client.ex b/lib/mixpanel/client.ex index 7185d86..9787078 100644 --- a/lib/mixpanel/client.ex +++ b/lib/mixpanel/client.ex @@ -113,7 +113,16 @@ defmodule Mixpanel.Client do @impl GenServer @spec init([option, ...]) :: {:ok, State.t()} def init(opts) do - {:ok, State.new(opts)} + Process.flag(:trap_exit, true) + state = State.new(opts) + + client_span = + Mixpanel.Telemetry.start_span(:client, %{}, %{ + base_url: State.base_url(state), + http_adapter: State.http_adapter(state) + }) + + {:ok, State.attach_span(state, client_span)} end @spec handle_cast( @@ -200,6 +209,12 @@ defmodule Mixpanel.Client do {:noreply, state} end + @impl GenServer + @spec terminate(reason, State.t()) :: :ok + when reason: :normal | :shutdown | {:shutdown, term} | term + def terminate(_reason, state), + do: Mixpanel.Telemetry.stop_span(State.span(state)) + defp put_token(events, project_token) when is_list(events), do: Enum.map(events, &put_token(&1, project_token)) diff --git a/lib/mixpanel/client/state.ex b/lib/mixpanel/client/state.ex index 14a9ab1..0e26df0 100644 --- a/lib/mixpanel/client/state.ex +++ b/lib/mixpanel/client/state.ex @@ -11,7 +11,7 @@ defmodule Mixpanel.Client.State do } @enforce_keys [:project_token, :base_url, :http_adapter] - defstruct [:project_token, :base_url, :http_adapter] + defstruct [:project_token, :base_url, :http_adapter, :span] def new(opts) do project_token = Keyword.fetch!(opts, :project_token) @@ -24,4 +24,18 @@ defmodule Mixpanel.Client.State do http_adapter: http_adapter } end + + @spec attach_span(t(), Mixpanel.Telemetry.t()) :: t() + def attach_span(state, span) do + %__MODULE__{state | span: span} + end + + @spec base_url(t()) :: base_url + def base_url(state), do: state.base_url + + @spec http_adapter(t()) :: module + def http_adapter(state), do: state.http_adapter + + @spec span(t()) :: Mixpanel.Telemetry.t() + def span(state), do: state.span end diff --git a/lib/mixpanel/telemetry.ex b/lib/mixpanel/telemetry.ex new file mode 100644 index 0000000..f9b0a02 --- /dev/null +++ b/lib/mixpanel/telemetry.ex @@ -0,0 +1,118 @@ +defmodule Mixpanel.Telemetry do + @moduledoc """ + The following telemetry spans are emitted by mixpanel_api_ex: + + ## `[:mixpanel_api_ex, :client, *]` + + Represents a Mixpanel API client is ready + + This span is started by the following event: + + * `[:mixpanel_api_ex, :client, :start]` + + Represents the start of the span + + This event contains the following measurements: + + * `monotonic_time` - The time of this event, in `:native` units + + This event contains the following metadata: + + * `base_url` - The URL which a client instance uses to communicate with + the Mixpanel API + * `http_adapter` - The HTTP adapter which a client instance uses to send + actual requests to the backend + + This span is ended by the following event: + + * `[:mixpanel_api_ex, :client, :stop]` + + Represents the end of the span + + This event contains the following measurements: + + * `monotonic_time`: The time of this event, in `:native` units + * `duration`: The span duration, in `:native` units + + This event contains the following metadata: + + * `base_url` - The URL which a client instance uses to communicate with + the Mixpanel API + * `http_adapter` - The HTTP adapter which a client instance uses to send + actual requests to the backend + + """ + + @enforce_keys [:span_name, :telemetry_span_context, :start_time, :start_metadata] + defstruct @enforce_keys + + @type t :: %__MODULE__{ + span_name: span_name, + telemetry_span_context: reference, + start_time: integer, + start_metadata: metadata + } + + @type span_name :: :client + @type metadata :: :telemetry.event_metadata() + + @typedoc false + @type measurements :: :telemetry.event_measurements() + + @typedoc false + @type event_name :: :ready + + @typedoc false + @type untimed_event_name :: :stop + + @app_name :mixpanel_api_ex + + @doc false + @spec start_span(span_name(), measurements(), metadata()) :: t() + def start_span(span_name, measurements, metadata) do + measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0) + telemetry_span_context = make_ref() + metadata = Map.put(metadata, :telemetry_span_context, telemetry_span_context) + _ = event([span_name, :start], measurements, metadata) + + %__MODULE__{ + span_name: span_name, + telemetry_span_context: telemetry_span_context, + start_time: measurements[:monotonic_time], + start_metadata: metadata + } + end + + @doc false + @spec stop_span(t(), measurements(), metadata()) :: :ok + def stop_span(span, measurements \\ %{}, metadata \\ %{}) do + measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0) + + measurements = + Map.put(measurements, :duration, measurements[:monotonic_time] - span.start_time) + + metadata = Map.merge(span.start_metadata, metadata) + + untimed_span_event(span, :stop, measurements, metadata) + end + + # @doc false + # @spec span_event(t(), event_name(), measurements(), metadata()) :: :ok + # def span_event(span, name, measurements \\ %{}, metadata \\ %{}) do + # end + + @doc false + @spec untimed_span_event(t(), event_name() | untimed_event_name(), measurements(), metadata()) :: + :ok + def untimed_span_event(span, name, measurements \\ %{}, metadata \\ %{}) do + metadata = Map.put(metadata, :telemetry_span_context, span.telemetry_span_context) + event([span.span_name, name], measurements, metadata) + end + + @spec monotonic_time() :: integer + defdelegate monotonic_time, to: System + + defp event(suffix, measurements, metadata) do + :telemetry.execute([@app_name | suffix], measurements, metadata) + end +end