From 5ed6f465aebc38ec9a10aa4f9911a4afcab77f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Niemier?= Date: Tue, 9 Oct 2018 19:20:04 +0200 Subject: [PATCH] Replace Statix with Prometheus Prometheus pull method of metrics gathering suits better such application as it doesn't require gatherer to be configure always. It also provides metrics in much more "standardized" way as prometheus_phoenix and prometheus_plugs are quite norm when coming to the respective library metrics gathering. There are also predefined Grafana's dashboards that can be found there: https://github.com/deadtrickster/beam-dashboards --- config/config.exs | 6 +- config/dev.exs | 2 +- lib/imager.ex | 17 ++-- lib/imager/application.ex | 11 ++- lib/imager/instrumenter/cache.ex | 15 ++++ lib/imager/instrumenter/processing.ex | 26 ++++++ lib/imager/instrumenter/storage.ex | 49 +++++++++++ lib/imager/stats.ex | 42 --------- lib/imager/store.ex | 17 ++-- lib/imager_web/controllers/health.ex | 4 +- lib/imager_web/endpoint.ex | 6 ++ lib/imager_web/instrumenter.ex | 30 ------- lib/imager_web/plug/metrics_exporter.ex | 85 +++++++++++++++++++ lib/imager_web/plug/pipeline_instrumenter.ex | 48 +++++++++++ mix.exs | 11 ++- mix.lock | 6 ++ priv/config.toml.template | 5 -- test/imager/stats_test.exs | 27 ------ .../imager_web/plug/metrics_exporter_test.exs | 39 +++++++++ 19 files changed, 310 insertions(+), 136 deletions(-) create mode 100644 lib/imager/instrumenter/cache.ex create mode 100644 lib/imager/instrumenter/processing.ex create mode 100644 lib/imager/instrumenter/storage.ex delete mode 100644 lib/imager/stats.ex delete mode 100644 lib/imager_web/instrumenter.ex create mode 100644 lib/imager_web/plug/metrics_exporter.ex create mode 100644 lib/imager_web/plug/pipeline_instrumenter.ex delete mode 100644 test/imager/stats_test.exs create mode 100644 test/imager_web/plug/metrics_exporter_test.exs diff --git a/config/config.exs b/config/config.exs index 371cecf..0df4ab8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -9,7 +9,7 @@ use Mix.Config config :imager, ImagerWeb.Endpoint, url: [host: "localhost"], render_errors: [view: ImagerWeb.Views.Error, accepts: ~w(json)], - instrumenters: [ImagerWeb.Instrumenter] + instrumenters: [Prometheus.PhoenixInstrumenter] # Configures Elixir's Logger config :logger, :console, @@ -21,10 +21,6 @@ config :phoenix, :format_encoders, json: Jason config :ex_aws, json_codec: Jason -config :vmstats, - sink: Imager.Stats, - base_key: "erlang" - config :sentry, included_environments: [:prod], release: Mix.Project.config()[:version], diff --git a/config/dev.exs b/config/dev.exs index 9356bc4..93f93e1 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -20,7 +20,7 @@ config :logger, :console, format: "[$level] $message\n" config :imager, :stores, %{ "local" => %{ store: {Imager.Store.Local, dir: "test/fixtures/"}, - cache: {Imager.Store.Local, dir: "tmp/cache/"} + cache: {Imager.Store.Blackhole, []} } } diff --git a/lib/imager.ex b/lib/imager.ex index 70c9389..ad73bd5 100644 --- a/lib/imager.ex +++ b/lib/imager.ex @@ -9,9 +9,9 @@ defmodule Imager do require Logger - alias Imager.Stats alias Imager.Store alias Imager.Tool + alias Imager.Instrumenter @type size :: non_neg_integer() | :unknown @type mime :: binary() @@ -24,7 +24,7 @@ defmodule Imager do def process(store, file_name, commands, opts \\ []) def process(%{store: store}, file_name, [], opts) do - Stats.increment("imager.passthrough", 1, tags: Stats.tags()) + Instrumenter.Cache.passthrough(store) Store.retrieve(store, file_name, opts) end @@ -32,17 +32,14 @@ defmodule Imager do def process(%{store: store, cache: cache}, file_name, commands, opts) do {mime, result_name} = Tool.result(file_name, commands) args = Tool.build_command(commands) - tags = Stats.tags(~w( - store:#{elem(store, 0)} - cache:#{elem(cache, 0)} - )) + + Instrumenter.Processing.command(commands) Logger.metadata(input: file_name, commands: inspect(commands)) with :error <- Store.retrieve(cache, result_name, opts), {:ok, {_, _, stream}} <- Store.retrieve(store, file_name, opts) do Logger.debug("Start processing") - Stats.increment("imager.process.started", 1, tags: tags) process = Porcelain.spawn(executable(), args, @@ -53,7 +50,7 @@ defmodule Imager do case Porcelain.Process.await(process) do {:ok, %{status: 0}} -> - Stats.increment("imager.process.succeeded", 1, tags: tags) + Instrumenter.Processing.succeeded(store) stream = process.out @@ -62,7 +59,7 @@ defmodule Imager do {:ok, {:unknown, mime, stream}} _ -> - Stats.increment("imager.process.failed", 1, tags: tags) + Instrumenter.Processing.failed(store) Logger.error(process.err) :failed @@ -70,7 +67,7 @@ defmodule Imager do else {:ok, {_, _, _}} = result -> Logger.debug("Cache hit") - Stats.increment("imager.process.cache_hit", 1, tags: tags) + Instrumenter.Cache.cache_hit(cache) result diff --git a/lib/imager/application.ex b/lib/imager/application.ex index e9d8af3..c279299 100644 --- a/lib/imager/application.ex +++ b/lib/imager/application.ex @@ -14,9 +14,10 @@ defmodule Imager.Application do Application.get_env(:imager, :sentry_dsn) ) - Imager.Stats.start() {:ok, _} = Logger.add_backend(Sentry.LoggerBackend) + prometheus() + opts = [strategy: :one_for_one, name: Imager.Supervisor] Supervisor.start_link(children, opts) end @@ -26,4 +27,12 @@ defmodule Imager.Application do :ok end + + defp prometheus do + :prometheus_registry.register_collector(:prometheus_process_collector) + + Imager.Instrumenter.Cache.setup() + Imager.Instrumenter.Processing.setup() + Imager.Instrumenter.Storage.setup() + end end diff --git a/lib/imager/instrumenter/cache.ex b/lib/imager/instrumenter/cache.ex new file mode 100644 index 0000000..9a3b32c --- /dev/null +++ b/lib/imager/instrumenter/cache.ex @@ -0,0 +1,15 @@ +defmodule Imager.Instrumenter.Cache do + def setup do + :prometheus_counter.declare( + name: :cache_hits_total, + help: "Count of cached images hits", + labels: [:type, :store] + ) + end + + def passthrough({store, _}), + do: :prometheus_counter.inc(:cache_hits_total, [:passthrough, store]) + + def cache_hit({store, _}), + do: :prometheus_counter.inc(:cache_hits_total, [:cache, store]) +end diff --git a/lib/imager/instrumenter/processing.ex b/lib/imager/instrumenter/processing.ex new file mode 100644 index 0000000..1b82dcc --- /dev/null +++ b/lib/imager/instrumenter/processing.ex @@ -0,0 +1,26 @@ +defmodule Imager.Instrumenter.Processing do + def setup do + :prometheus_counter.declare( + name: :processed_total, + help: "Count of processed images", + labels: [:status, :store] + ) + + :prometheus_counter.declare( + name: :process_commands_total, + help: "Processing commands defined", + labels: [:command, :argument] + ) + end + + def succeeded({store, _}), + do: :prometheus_counter.inc(:processed_total, ["ok", store]) + + def failed({store, _}), + do: :prometheus_counter.inc(:processed_total, ["failed", store]) + + def command({command, argument}), + do: :prometheus_counter.inc(:process_commands_total, [command, argument]) + + def command(list) when is_list(list), do: Enum.each(list, &command/1) +end diff --git a/lib/imager/instrumenter/storage.ex b/lib/imager/instrumenter/storage.ex new file mode 100644 index 0000000..08be624 --- /dev/null +++ b/lib/imager/instrumenter/storage.ex @@ -0,0 +1,49 @@ +defmodule Imager.Instrumenter.Storage do + def setup do + :prometheus_counter.declare( + name: :store_retrieved_total, + help: "Count of retrieves from the given store", + labels: [:store] + ) + + :prometheus_counter.declare( + name: :store_saved_total, + help: "Count of saves into the given store", + labels: [:store] + ) + + :prometheus_summary.declare( + name: :store_retrieved_bytes, + help: "Bytes retrieved from the store", + labels: [:store] + ) + + :prometheus_summary.declare( + name: :store_saved_bytes, + help: "Saved bytes to given store", + labels: [:store] + ) + end + + def retrieved(stream, store) do + :prometheus_counter.inc(:store_retrieved_total, [store]) + + Stream.each( + stream, + &:prometheus_summary.observe( + :store_retrieved_bytes, + [store], + byte_size(&1) + ) + ) + end + + def saved(stream, store) do + :prometheus_counter.inc(:store_saved_total, [store]) + + Stream.each( + stream, + &:prometheus_summary.observe(:store_saved_bytes, [store], byte_size(&1)) + ) + end +end diff --git a/lib/imager/stats.ex b/lib/imager/stats.ex deleted file mode 100644 index 33f0924..0000000 --- a/lib/imager/stats.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Imager.Stats do - @moduledoc """ - StatsD statistics gatherer. - """ - - @behaviour :vmstats_sink - - use Statix, runtime_config: true - - def start do - if config = Application.get_env(:imager, :stats) do - Application.put_env(:statix, __MODULE__, - host: Map.fetch!(config, :host), - port: Map.fetch!(config, :port) - ) - end - - connect() - {:ok, _} = Application.ensure_all_started(:vmstats, :permanent) - - :ok - end - - def collect(type, key, value) do - do_collect( - type, - List.to_string(key), - value, - tags() - ) - end - - def tags(tags \\ []) when is_list(tags) do - ["app:imager", "version:#{version()}" | tags] - end - - defp do_collect(:counter, key, value, opts), do: increment(key, value, opts) - defp do_collect(:gauge, key, value, opts), do: gauge(key, value, opts) - defp do_collect(:timing, key, value, opts), do: histogram(key, value, opts) - - def version, do: :imager |> Application.spec(:vsn) |> List.to_string() -end diff --git a/lib/imager/store.ex b/lib/imager/store.ex index 0b7e677..37a1afd 100644 --- a/lib/imager/store.ex +++ b/lib/imager/store.ex @@ -4,7 +4,7 @@ defmodule Imager.Store do in application. """ - alias Imager.Stats + alias Imager.Instrumenter @type size :: non_neg_integer() @type stream :: Enumerable.t() @@ -31,13 +31,9 @@ defmodule Imager.Store do @spec retrieve(store, binary, keyword) :: {:ok, {size, mime, stream}} | :error def retrieve({store, glob_opts}, path, options) do - Stats.increment( - "imager.store.retrieve", - 1, - Stats.tags(~w(module:#{store})) - ) - - store.retrieve(path, Keyword.merge(glob_opts, options)) + with {:ok, {size, mime, stream}} <- + store.retrieve(path, Keyword.merge(glob_opts, options)), + do: {:ok, {size, mime, Instrumenter.Storage.retrieved(stream, store)}} end @doc """ @@ -45,7 +41,8 @@ defmodule Imager.Store do """ @spec store(stream, store, mime, binary, keyword) :: stream def store(stream, {store, glob_opts}, mime, path, options) do - Stats.increment("imager.store.store", 1, Stats.tags(~w(module:#{store}))) - store.store(path, mime, stream, Keyword.merge(glob_opts, options)) + path + |> store.store(mime, stream, Keyword.merge(glob_opts, options)) + |> Instrumenter.Storage.saved(store) end end diff --git a/lib/imager_web/controllers/health.ex b/lib/imager_web/controllers/health.ex index 705e8ff..d34e892 100644 --- a/lib/imager_web/controllers/health.ex +++ b/lib/imager_web/controllers/health.ex @@ -6,8 +6,10 @@ defmodule ImagerWeb.Controllers.Health do def get(conn, _params) do json(conn, %{ status: "pass", - version: Imager.Stats.version(), + version: version(), description: @description }) end + + defp version, do: Application.spec(:imager, :vsn) |> List.to_string() end diff --git a/lib/imager_web/endpoint.ex b/lib/imager_web/endpoint.ex index 64b7ad6..41bf3bb 100644 --- a/lib/imager_web/endpoint.ex +++ b/lib/imager_web/endpoint.ex @@ -2,6 +2,9 @@ defmodule ImagerWeb.Endpoint do use Phoenix.Endpoint, otp_app: :imager use Sentry.Phoenix.Endpoint + plug(ImagerWeb.Plug.PipelineInstrumenter) + plug(ImagerWeb.Plug.MetricsExporter) + # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do @@ -27,6 +30,9 @@ defmodule ImagerWeb.Endpoint do configuration should be loaded from the system environment. """ def init(_key, config) do + ImagerWeb.Plug.PipelineInstrumenter.setup() + ImagerWeb.Plug.MetricsExporter.setup() + port = System.get_env("PORT") || Application.get_env(:imager, :port) || raise "expected the PORT environment variable to be set" diff --git a/lib/imager_web/instrumenter.ex b/lib/imager_web/instrumenter.ex deleted file mode 100644 index e8b412b..0000000 --- a/lib/imager_web/instrumenter.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule ImagerWeb.Instrumenter do - @moduledoc """ - Instrument application. - - This module gathers statistics about running application and sends them - to StatsD compatible server. - """ - - def phoenix_controller_call(:start, _, %{conn: conn}) do - Imager.Stats.tags(~w[ - host:#{conn.host} - method:#{conn.method} - scheme:#{conn.scheme} - controller:#{module_name(conn.private[:phoenix_controller])} - action:#{conn.private[:phoenix_action]} - format:#{conn.private[:phoenix_format] || "unknown"} - ]) - end - - def phoenix_controller_call(:stop, time, tags) do - Imager.Stats.histogram("phoenix.request.duration", time, tags: tags) - end - - defp module_name(nil), do: nil - defp module_name("Elixir." <> name), do: name - defp module_name(name) when is_bitstring(name), do: name - - defp module_name(name) when is_atom(name), - do: module_name(Atom.to_string(name)) -end diff --git a/lib/imager_web/plug/metrics_exporter.ex b/lib/imager_web/plug/metrics_exporter.ex new file mode 100644 index 0000000..6812d8a --- /dev/null +++ b/lib/imager_web/plug/metrics_exporter.ex @@ -0,0 +1,85 @@ +defmodule ImagerWeb.Plug.MetricsExporter do + @behaviour Plug + + import Plug.Conn + + require Logger + + def setup do + :prometheus_summary.declare( + name: :telemetry_scrape_duration_ms, + help: "Scrape duration", + labels: [:content_type] + ) + + :prometheus_summary.declare( + name: :telemetry_scrape_size_bytes, + help: "Scrape size, uncompressed", + labels: [:content_type] + ) + end + + def init(opts), do: opts + + def call(conn, opts) do + opts = + opts + |> Keyword.merge(Application.get_env(:imager, :prometheus, [])) + |> Keyword.put_new(:endpoint, "/metrics") + |> Keyword.update(:format, :prometheus_text_format, &parse_format/1) + + endpoint = Keyword.fetch!(opts, :endpoint) + + case conn.request_path do + ^endpoint -> send_stats(conn, opts) + _ -> conn + end + end + + defp send_stats(conn, opts) do + format = Keyword.fetch!(opts, :format) + + {type, data} = scrape_data(format) + + conn + |> put_resp_content_type(type) + |> send_resp(200, data) + |> halt() + end + + defp scrape_data(format) do + start = :erlang.monotonic_time(:millisecond) + + try do + format.format(:default) + else + value -> + stop = :erlang.monotonic_time(:millisecond) + type = format.content_type() + + :prometheus_summary.observe( + :telemetry_scrape_duration_ms, + [type], + stop - start + ) + + :prometheus_summary.observe( + :telemetry_scrape_size_bytes, + [type], + :erlang.iolist_size(value) + ) + + {type, value} + end + end + + defp parse_format(nil), do: :prometheus_text_format + defp parse_format("text"), do: :prometheus_text_format + defp parse_format("protobuf"), do: :prometheus_protobuf_format + + defp parse_format(format) do + Logger.warn("Unknown format #{inspect(format)}, falling back to \"text\"") + + :prometheus_text_format + end +end diff --git a/lib/imager_web/plug/pipeline_instrumenter.ex b/lib/imager_web/plug/pipeline_instrumenter.ex new file mode 100644 index 0000000..1956e8d --- /dev/null +++ b/lib/imager_web/plug/pipeline_instrumenter.ex @@ -0,0 +1,48 @@ +defmodule ImagerWeb.Plug.PipelineInstrumenter do + @behaviour Plug + + @labels [:status_class, :method, :host, :scheme] + + def setup do + :prometheus_counter.declare( + name: :http_requests_total, + help: "Total number of HTTP requests made.", + labels: @labels + ) + + :prometheus_histogram.declare( + name: :http_request_duration_microseconds, + help: "The HTTP request latencies in microseconds.", + labels: @labels, + buckets: :prometheus_http.microseconds_duration_buckets() + ) + end + + def init(opts), do: opts + + def call(conn, _opts) do + start = :erlang.monotonic_time(:microsecond) + + Plug.Conn.register_before_send(conn, fn conn -> + labels = for label <- @labels, do: value_for(label, conn) + + :prometheus_counter.inc(:http_requests_total, labels) + + stop = :erlang.monotonic_time(:microsecond) + diff = stop - start + + :prometheus_histogram.observe( + :http_request_duration_microseconds, + labels, + diff + ) + + conn + end) + end + + defp value_for(:status_class, conn), + do: :prometheus_http.status_class(conn.status) + + defp value_for(field, conn), do: Map.fetch!(conn, field) +end diff --git a/mix.exs b/mix.exs index f7d4f10..5e2d74d 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,6 @@ defmodule Imager.Mixfile do compilers: [:phoenix] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, deps: deps(), - dialyzer: [plt_add_apps: [:vmstats]], test_coverage: [tool: ExCoveralls], preferred_cli_env: [ coveralls: :test, @@ -36,7 +35,7 @@ defmodule Imager.Mixfile do [ mod: {Imager.Application, []}, extra_applications: [:logger, :inets, :runtime_tools], - included_applications: [:vmstats] + included_applications: [] ] end @@ -53,13 +52,17 @@ defmodule Imager.Mixfile do {:phoenix_pubsub, "~> 1.0"}, {:cowboy, "~> 1.0"}, {:distillery, "~> 2.0"}, - {:statix, ">= 0.0.0"}, + {:prometheus, "~> 4.1"}, + {:prometheus_phoenix, "~> 1.2.0"}, + {:prometheus_process_collector, + github: "deadtrickster/prometheus_process_collector", + override: true, + manager: :rebar3}, {:ex_aws, "~> 2.0"}, {:ex_aws_s3, "~> 2.0"}, {:hackney, "~> 1.9"}, {:sweet_xml, "~> 0.6"}, {:jason, ">= 0.0.0"}, - {:vmstats, "~> 2.2", runtime: false}, {:sentry, "~> 7.0"}, {:porcelain, "~> 2.0.3"}, {:jose, "~> 1.8"}, diff --git a/mix.lock b/mix.lock index e4020d5..969e243 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "accept": {:hex, :accept, "0.3.3", "548ebb6fb2e8b0d170e75bb6123aea6ceecb0189bb1231eeadf52eac08384a97", [:rebar3], [], "hexpm"}, "artificery": {:hex, :artificery, "0.2.6", "f602909757263f7897130cbd006b0e40514a541b148d366ad65b89236b93497a", [:mix], [], "hexpm"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, @@ -30,6 +31,11 @@ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm"}, + "prometheus": {:hex, :prometheus, "4.1.0", "3bb851df031c204d1c94bf55fff2ecc9ab834f0236e64c080c9d5945b48d428d", [:mix, :rebar3], [], "hexpm"}, + "prometheus_ex": {:hex, :prometheus_ex, "3.0.3", "5d722263bb1f7a9b1d02554de42e61ea672b4e3c07c3f74e23ce35ab5e111cfa", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, + "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, + "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"}, + "prometheus_process_collector": {:git, "https://github.com/deadtrickster/prometheus_process_collector.git", "fa11b50f2f81d4e3bed70298fcde54e30670e195", []}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "sentry": {:hex, :sentry, "7.0.0", "12421006c29d4fd6dbd822316868e542f49c934ba9bf18862304147779d4d0fc", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, diff --git a/priv/config.toml.template b/priv/config.toml.template index 3dfffa7..bfbcc93 100644 --- a/priv/config.toml.template +++ b/priv/config.toml.template @@ -8,11 +8,6 @@ # # Sentry DSN key used to report errors # sentry_dsn = "" -# -# StatsD configuration -# [stats] -# host = "localhost" # host send stats to -# port = 8125 # UDP port to send stats to # # Store definition # diff --git a/test/imager/stats_test.exs b/test/imager/stats_test.exs deleted file mode 100644 index 14c65c6..0000000 --- a/test/imager/stats_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Imager.StatsTest do - use ExUnit.Case, async: true - use ExUnitProperties - - property "tags contain all provided entries" do - check all tags <- list_of(string(:alphanumeric)) do - generated = Imager.Stats.tags(tags) - - for tag <- tags do - assert tag in generated - end - end - end - - property "tags contains default tags" do - check all tags <- list_of(string(:alphanumeric)) do - generated = Imager.Stats.tags(tags) - - assert "app:imager" in generated - - assert Enum.find(generated, fn - "version:" <> _ -> true - _ -> false - end) - end - end -end diff --git a/test/imager_web/plug/metrics_exporter_test.exs b/test/imager_web/plug/metrics_exporter_test.exs new file mode 100644 index 0000000..3a9e951 --- /dev/null +++ b/test/imager_web/plug/metrics_exporter_test.exs @@ -0,0 +1,39 @@ +defmodule ImagerWeb.Plug.MetricsExporterTest do + use ExUnit.Case, async: true + use Plug.Test + use ExUnitProperties + + alias ImagerWeb.Plug.MetricsExporter, as: Subject + + defp path do + gen all segments <- + list_of(string(:alphanumeric, min_length: 1), min_length: 1) do + "/" <> Path.join(segments) + end + end + + defp http_method do + ~w(get post delete patch put head) + |> Enum.map(&constant/1) + |> one_of() + end + + property "non-metric endpoints are passed through" do + opts = Subject.init(endpoint: "/metric") + + check all path <- path(), + method <- http_method(), + path != "/metric" do + conn = conn(method, path) + + assert conn == Subject.call(conn, opts) + end + end + + test "metric endpoint return 200" do + opts = Subject.init(endpoint: "/metric") + conn = conn(:get, "/metric") + + assert {200, _, _} = sent_resp(Subject.call(conn, opts)) + end +end