defmodule SmartCity.Data.Timing do
@moduledoc """
Timing struct for adding timing metrics to `SmartCity.Data` messages
"""
@type t :: %SmartCity.Data.Timing{
app: String.t(),
label: String.t(),
start_time: DateTime.t(),
end_time: DateTime.t()
}
@enforce_keys [:app, :label]
@derive Jason.Encoder
defstruct app: nil, label: nil, start_time: nil, end_time: nil
@validate_keys [:app, :label, :start_time, :end_time]
@doc """
Creates a new `SmartCity.Data.Timing` struct, passing in all fields.
Returns a `SmartCity.Data.Timing` struct or raises `ArgumentError`.
## Parameters
- app: application for which timing metrics are being measured
- label: description of timing measurement
- start_time: time when measurement has begun
- end_time: time when measurement has finished
## Examples
iex> SmartCity.Data.Timing.new("foo", "bar", "not_validated", "not_validated")
%SmartCity.Data.Timing{
app: "foo",
label: "bar",
start_time: "not_validated",
end_time: "not_validated"
}
"""
@spec new(term(), term(), term(), term()) :: SmartCity.Data.Timing.t()
def new(app, label, start_time, end_time) do
new(app: app, label: label, start_time: start_time, end_time: end_time)
end
@doc """
Creates a new `SmartCity.Data.Timing` from opts.
Returns a `SmartCity.Data.Timing` struct or raises `ArgumentError`
## Parameters
- opts: Keyword list or map containing struct attributes
Required keys: #{@enforce_keys |> Enum.map_join(", ", &"`#{Atom.to_string(&1)}`")}
See `Kernel.struct!/2`.
"""
@spec new(
%{
:app => term(),
:label => term(),
optional(:start_time) => term(),
optional(:end_time) => term()
}
| list()
) :: SmartCity.Data.Timing.t()
def new(opts) do
struct!(__MODULE__, opts)
end
@doc """
Gets the current time. This function should always be used for generating times to be used in timings to ensure consistency across all services.
Returns current UTC Time in ISO8601 format
"""
@spec current_time() :: String.t()
def current_time do
DateTime.utc_now() |> DateTime.to_iso8601()
end
@doc """
Validate that all required keys are present and valid (not nil).
Set by `@validate_keys` module attribute.
Currently checks: #{@enforce_keys |> Enum.map_join(", ", &"`#{Atom.to_string(&1)}`")}
Returns `{:ok, timing}` on success or `{:error, reason}` on failure
## Parameters
- timing: The `SmartCity.Data.Timing` struct to validate
"""
@spec validate(SmartCity.Data.Timing.t()) ::
{:ok, SmartCity.Data.Timing.t()} | {:error, String.t()}
def validate(%__MODULE__{} = timing) do
case check_keys(timing, @validate_keys) do
[] -> {:ok, timing}
errors -> {:error, join_error_message(errors)}
end
end
@doc """
Validate that all required keys are present and valid (not nil).
Returns `timing` on success, or raises `ArgumentError` on failure
See `validate/1`
"""
@spec validate!(SmartCity.Data.Timing.t()) :: SmartCity.Data.Timing.t()
def validate!(%__MODULE__{} = timing) do
case validate(timing) do
{:ok, timing} -> timing
{:error, reason} -> raise ArgumentError, reason
end
end
@doc """
Wraps the results of a function call with measured timing information
Returns {:ok, `result`, `timing`} on success, or {:error, `reason`} on failure
"""
@spec measure(String.t(), String.t(), (() -> {:ok, term()} | {:error, term()})) ::
{:ok, term(), SmartCity.Data.Timing.t()} | {:error, String.t()}
def measure(app, label, function) when is_function(function) do
start_time = DateTime.utc_now()
case function.() do
{:ok, result} ->
{:ok, result, new(%{app: app, label: label, start_time: start_time, end_time: DateTime.utc_now()})}
{:error, reason} ->
{:error, reason}
reason ->
{:error, reason}
end
end
defp check_keys(timing, keys) do
keys
|> Enum.map(&check_key(timing, &1))
|> List.flatten()
end
defp check_key(timing, key) do
case Map.get(timing, key, :missing_key) do
:missing_key -> {:missing_key, key}
nil -> {:invalid, key}
_ -> []
end
end
defp join_error_message(errors) do
error_msg =
errors
|> Enum.map_join(", ", fn {reason, key} -> "#{Atom.to_string(key)}(#{Atom.to_string(reason)})" end)
"Errors with: #{error_msg}"
end
end