lib/mix/tasks/gen.metrics.ex

defmodule Mix.Tasks.PrometheusTelemetry.Gen.Metrics do
  @shortdoc "Generates Metrics modules"
  @moduledoc """
  This can be used to generate metrics modules

  The format used is
  ```elixir
  <metric_type>:<metric_name>:<event_name>:<measurement_unit>:tags:<tag_name>:<tag_name>
  ```

  ### Example
  ```bash
  $ mix prometheus_telemetry.gen.metrics MyApp.Metrics.Type counter:event.name.measurement.count:event.name.count:count:tags:profile:region
  ```

  The following metrics have been implemented:
  `counter`, `distribution`, `last_value` and `sum`

  With `distribution` you can also provide a second parameter of milliseconds, seconds or microseconds
  """

  use Mix.Task

  def run([]) do
    Mix.raise("Must supply arguments to prometheus_telemetry.gen.metrics")
  end

  def run([metrics_module | measurements]) do

    measurements
      |> Enum.map(&(&1 |> String.split(":") |> parse_measurements))
      |> build_metrics_module_from_measurements(metrics_module)
      |> write_metrics_file(metrics_module)
  end

  defp parse_measurements(["counter", metric_name, event_name, measurement | tags]) do
    [
      type: :counter,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      tags: parse_tags(tags)
    ]
  end

  defp parse_measurements(["distribution", "milliseconds", metric_name, event_name, measurement | tags]) do
    [
      type: :distribution,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      unit: :milliseconds, tags: parse_tags(tags)
    ]
  end

  defp parse_measurements(["distribution", "microseconds", metric_name, event_name, measurement | tags]) do
    [
      type: :distribution,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      unit: :microseconds, tags: parse_tags(tags)
    ]
  end

  defp parse_measurements(["distribution", "seconds", metric_name, event_name, measurement | tags]) do
    [
      type: :distribution,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      tags: parse_tags(tags)
    ]
  end

  defp parse_measurements(["distribution", metric_name, event_name, measurement | tags]) do
    [
      type: :distribution,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      unit: :milliseconds, tags: parse_tags(tags)
    ]
  end

  defp parse_measurements(["last_value", metric_name, event_name, measurement | tags]) do
    [
      type: :last_value,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      tags: parse_tags(tags)
    ]
  end

  defp parse_measurements(["sum", metric_name, event_name, measurement | tags]) do
    [
      type: :sum,
      metric_name: metric_name,
      event_name: parse_event_name(event_name),
      event_function: parse_function_name(event_name, measurement),
      measurement: String.to_atom(measurement),
      tags: parse_tags(tags)
    ]
  end

  defp parse_tags(["tags" | tags]) when tags !== [] do
    Enum.map(tags, &String.to_atom/1)
  end

  defp parse_tags(_) do
    []
  end

  defp parse_event_name(event_name) do
    event_name |> String.split(".") |> Enum.map(&String.to_atom/1)
  end

  defp parse_function_name(event_name, measurement) do
    event_name
      |> String.replace(".", "_")
      |> String.trim_trailing("_#{measurement}")
      |> String.trim_trailing(measurement)
  end

  defp build_metrics_module_from_measurements(measurements, metrics_module) do
    :prometheus_telemetry
      |> :code.priv_dir
      |> Path.join("metric_template.ex.eex")
      |> EEx.eval_file(
        assigns: %{
          metrics: measurements,
          module_name: metrics_module,
          metrics_imports: measurement_metrics_imports(measurements)
        }
      )
      |> Code.format_string!
  end

  defp measurement_metrics_imports(measurements) do
    measurements
      |> Enum.group_by(&(&1[:type]))
      |> Map.keys
      |> Enum.map(fn type -> {type, 2} end)
  end

  defp write_metrics_file(metrics_file_contents, metrics_module) do
    file_path = module_file_path(metrics_module)

    Mix.Generator.create_file(file_path, metrics_file_contents)
  end

  defp module_file_path(metrics_module) do
    metrics_path = metrics_module
      |> String.split(".")
      |> Enum.map(&Macro.underscore/1)
      |> then(&List.update_at(&1, length(&1) - 1, fn file_name -> "#{file_name}.ex" end))

    Path.join(["lib" | metrics_path])
  end
end