From eb04f3336dac125bc0926f2fe8401c2bb0b8cce4 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 10:42:08 +0000 Subject: [PATCH 01/14] Prometheus + JSON logging for grafana --- compose.saas.yml | 4 + server/config/runtime.exs | 25 ++++ .../controllers/api/auth_controller.ex | 24 ++++ .../controllers/api/stack_controller.ex | 12 ++ .../controllers/metrics_controller.ex | 19 +++ server/lib/ethui_web/endpoint.ex | 1 + server/lib/ethui_web/plugs/log_metadata.ex | 68 ++++++++++ server/lib/ethui_web/router.ex | 5 + server/lib/ethui_web/telemetry.ex | 120 ++++++++++++------ server/mix.exs | 2 + server/mix.lock | 2 + 11 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 server/lib/ethui_web/controllers/metrics_controller.ex create mode 100644 server/lib/ethui_web/plugs/log_metadata.ex diff --git a/compose.saas.yml b/compose.saas.yml index 60edcc0..b8d562b 100644 --- a/compose.saas.yml +++ b/compose.saas.yml @@ -35,6 +35,10 @@ services: - 'traefik.http.routers.ethui-stacks-secure.tls.domains[0].main=stacks.ethui.dev' - 'traefik.http.routers.ethui-stacks-secure.tls.domains[0].sans=*.stacks.ethui.dev' + # enabling scraping from alloy/prometheus + - "prometheus.scrape=true" + - "prometheus.port=4000" + volumes: data: diff --git a/server/config/runtime.exs b/server/config/runtime.exs index d2cce47..5fe7a77 100644 --- a/server/config/runtime.exs +++ b/server/config/runtime.exs @@ -81,6 +81,31 @@ if config_env() == :prod do config :ethui, :jwt_secret, jwt_secret + # JSON logging configuration for production + config :logger, :default_handler, + formatter: + {LoggerJSON.Formatters.Basic, + metadata: :all, exclude_metadata: [:domain, :erl_level, :gl, :time]} + + config :logger, :console, + format: {LoggerJSON.Formatters.Basic, :format}, + metadata: [ + :request_id, + :user_id, + :stack_slug, + :remote_ip, + :method, + :path, + :status, + :duration, + :pid, + :application, + :module, + :function, + :file, + :line + ] + if is_saas? do config :ethui, Ethui.Mailer, adapter: Swoosh.Adapters.Mua, diff --git a/server/lib/ethui_web/controllers/api/auth_controller.ex b/server/lib/ethui_web/controllers/api/auth_controller.ex index 700970b..0e4bf96 100644 --- a/server/lib/ethui_web/controllers/api/auth_controller.ex +++ b/server/lib/ethui_web/controllers/api/auth_controller.ex @@ -12,6 +12,12 @@ defmodule EthuiWeb.Api.AuthController do """ def send_code(conn, %{"email" => email}) do with {:ok, _user} <- Accounts.send_verification_code(email) do + :telemetry.execute( + [:ethui, :auth, :code_sent], + %{count: 1}, + %{email: email} + ) + render(conn, :send_code, message: "Verification code sent") end end @@ -24,12 +30,30 @@ defmodule EthuiWeb.Api.AuthController do def verify_code(conn, %{"email" => email, "code" => code}) do case Accounts.verify_code_and_generate_token(email, code) do {:ok, token} -> + :telemetry.execute( + [:ethui, :auth, :code_verified], + %{count: 1}, + %{status: :success, email: email} + ) + render(conn, :verify_code, token: token) {:error, :invalid_code} -> + :telemetry.execute( + [:ethui, :auth, :code_verified], + %{count: 1}, + %{status: :invalid_code, email: email} + ) + {:error, "Invalid or expired verification code"} {:error, _reason} -> + :telemetry.execute( + [:ethui, :auth, :code_verified], + %{count: 1}, + %{status: :error, email: email} + ) + {:error, "Verification failed"} end end diff --git a/server/lib/ethui_web/controllers/api/stack_controller.ex b/server/lib/ethui_web/controllers/api/stack_controller.ex index 9bdf702..c6e0320 100644 --- a/server/lib/ethui_web/controllers/api/stack_controller.ex +++ b/server/lib/ethui_web/controllers/api/stack_controller.ex @@ -32,6 +32,12 @@ defmodule EthuiWeb.Api.StackController do with {:ok, stack} <- Stacks.create_stack(user, params), _ <- Server.create(stack) do + :telemetry.execute( + [:ethui, :stacks, :created], + %{count: 1}, + %{user_id: user && user.id, stack_slug: stack.slug} + ) + conn |> put_status(:created) |> render(:create, stack: stack) @@ -45,6 +51,12 @@ defmodule EthuiWeb.Api.StackController do :ok <- authorize_user_access(user, stack), _ <- Server.destroy(stack), {:ok, _} <- Stacks.delete_stack(stack) do + :telemetry.execute( + [:ethui, :stacks, :deleted], + %{count: 1}, + %{user_id: user && user.id, stack_slug: slug} + ) + send_resp(conn, :no_content, "") else nil -> diff --git a/server/lib/ethui_web/controllers/metrics_controller.ex b/server/lib/ethui_web/controllers/metrics_controller.ex new file mode 100644 index 0000000..0315d32 --- /dev/null +++ b/server/lib/ethui_web/controllers/metrics_controller.ex @@ -0,0 +1,19 @@ +defmodule EthuiWeb.MetricsController do + use EthuiWeb, :controller + + @moduledoc """ + Exposes Prometheus metrics for scraping. + + This endpoint is designed to be accessible only within the internal Docker/Dokploy network, + not via external Traefik routing. Prometheus should scrape metrics directly from the + application on port 4000. + """ + + def index(conn, _params) do + metrics = TelemetryMetricsPrometheus.Core.scrape(EthuiWeb.Telemetry.Prometheus) + + conn + |> put_resp_content_type("text/plain") + |> send_resp(200, metrics) + end +end diff --git a/server/lib/ethui_web/endpoint.ex b/server/lib/ethui_web/endpoint.ex index 1bb3457..3005147 100644 --- a/server/lib/ethui_web/endpoint.ex +++ b/server/lib/ethui_web/endpoint.ex @@ -34,6 +34,7 @@ defmodule EthuiWeb.Endpoint do cookie_key: "request_logger" plug Plug.RequestId + plug EthuiWeb.Plugs.LogMetadata plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] plug CORSPlug, origin: ["*"] diff --git a/server/lib/ethui_web/plugs/log_metadata.ex b/server/lib/ethui_web/plugs/log_metadata.ex new file mode 100644 index 0000000..342c7cd --- /dev/null +++ b/server/lib/ethui_web/plugs/log_metadata.ex @@ -0,0 +1,68 @@ +defmodule EthuiWeb.Plugs.LogMetadata do + @moduledoc """ + Adds request-specific metadata to logs for structured JSON logging. + + This plug enriches logs with contextual information including: + - Request ID, remote IP, HTTP method, and path + - User ID (if authenticated) + - Stack slug (if available from subdomain routing) + - Response status and duration (added before sending response) + """ + + import Plug.Conn + require Logger + + def init(opts), do: opts + + def call(conn, _opts) do + Logger.metadata( + request_id: get_request_id(conn), + remote_ip: format_ip(conn.remote_ip), + method: conn.method, + path: conn.request_path + ) + + # Add user_id if authenticated + case conn.assigns[:current_user] do + %{id: user_id} -> Logger.metadata(user_id: user_id) + _ -> :ok + end + + # Add stack_slug if available (from subdomain routing) + case conn.assigns[:stack] do + %{slug: slug} -> Logger.metadata(stack_slug: slug) + _ -> :ok + end + + register_before_send(conn, fn conn -> + # Add response metadata before sending + Logger.metadata( + status: conn.status, + duration: calculate_duration(conn) + ) + conn + end) + end + + defp get_request_id(conn) do + case get_resp_header(conn, "x-request-id") do + [request_id] -> request_id + _ -> Logger.metadata()[:request_id] + end + end + + defp format_ip({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}" + defp format_ip(ip), do: inspect(ip) + + defp calculate_duration(conn) do + case conn.private[:phoenix_endpoint_start] do + %{system: start} -> + System.monotonic_time() + |> Kernel.-(start) + |> System.convert_time_unit(:native, :microsecond) + + _ -> + nil + end + end +end diff --git a/server/lib/ethui_web/router.ex b/server/lib/ethui_web/router.ex index 7dbcc4b..248b90b 100644 --- a/server/lib/ethui_web/router.ex +++ b/server/lib/ethui_web/router.ex @@ -77,6 +77,11 @@ defmodule EthuiWeb.Router do end end + # Metrics endpoint - accessible only within internal Docker network + scope "/" do + get "/metrics", EthuiWeb.MetricsController, :index + end + scope "/", EthuiWeb do pipe_through :proxy diff --git a/server/lib/ethui_web/telemetry.ex b/server/lib/ethui_web/telemetry.ex index 0211a5a..310e30a 100644 --- a/server/lib/ethui_web/telemetry.ex +++ b/server/lib/ethui_web/telemetry.ex @@ -11,9 +11,9 @@ defmodule EthuiWeb.Telemetry do children = [ # Telemetry poller will execute the given period measurements # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics - {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} - # Add reporters as children of your supervision tree. - # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}, + # Prometheus metrics reporter + {TelemetryMetricsPrometheus.Core, metrics: metrics(), name: __MODULE__.Prometheus} ] Supervisor.init(children, strategy: :one_for_one) @@ -22,64 +22,110 @@ defmodule EthuiWeb.Telemetry do def metrics do [ # Phoenix Metrics - summary("phoenix.endpoint.start.system_time", - unit: {:native, :millisecond} - ), - summary("phoenix.endpoint.stop.duration", - unit: {:native, :millisecond} + distribution("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond}, + description: "Phoenix endpoint response time", + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000]] ), - summary("phoenix.router_dispatch.start.system_time", + distribution("phoenix.router_dispatch.stop.duration", tags: [:route], - unit: {:native, :millisecond} + unit: {:native, :millisecond}, + description: "Phoenix router dispatch time by route", + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000]] ), - summary("phoenix.router_dispatch.exception.duration", + distribution("phoenix.router_dispatch.exception.duration", tags: [:route], - unit: {:native, :millisecond} + unit: {:native, :millisecond}, + description: "Phoenix router exception duration", + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000]] ), - summary("phoenix.router_dispatch.stop.duration", - tags: [:route], - unit: {:native, :millisecond} + distribution("phoenix.socket_connected.duration", + unit: {:native, :millisecond}, + description: "WebSocket connection time", + reporter_options: [buckets: [100, 250, 500, 1000, 2500, 5000]] ), - summary("phoenix.socket_connected.duration", - unit: {:native, :millisecond} + sum("phoenix.socket_drain.count", + description: "WebSocket drain count" ), - sum("phoenix.socket_drain.count"), - summary("phoenix.channel_joined.duration", - unit: {:native, :millisecond} + distribution("phoenix.channel_joined.duration", + unit: {:native, :millisecond}, + description: "Channel join duration", + reporter_options: [buckets: [100, 250, 500, 1000, 2500, 5000]] ), - summary("phoenix.channel_handled_in.duration", + distribution("phoenix.channel_handled_in.duration", tags: [:event], - unit: {:native, :millisecond} + unit: {:native, :millisecond}, + description: "Channel message handling duration", + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000]] ), # Database Metrics - summary("ethui.repo.query.total_time", + distribution("ethui.repo.query.total_time", unit: {:native, :millisecond}, - description: "The sum of the other measurements" + description: "Total database query time", + reporter_options: [buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000]] ), - summary("ethui.repo.query.decode_time", + distribution("ethui.repo.query.decode_time", unit: {:native, :millisecond}, - description: "The time spent decoding the data received from the database" + description: "Time spent decoding database results", + reporter_options: [buckets: [1, 5, 10, 25, 50, 100, 250]] ), - summary("ethui.repo.query.query_time", + distribution("ethui.repo.query.query_time", unit: {:native, :millisecond}, - description: "The time spent executing the query" + description: "Time spent executing database query", + reporter_options: [buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000]] ), - summary("ethui.repo.query.queue_time", + distribution("ethui.repo.query.queue_time", unit: {:native, :millisecond}, - description: "The time spent waiting for a database connection" + description: "Time spent waiting for database connection", + reporter_options: [buckets: [1, 5, 10, 25, 50, 100, 250]] ), - summary("ethui.repo.query.idle_time", + distribution("ethui.repo.query.idle_time", unit: {:native, :millisecond}, - description: - "The time the connection spent waiting before being checked out for the query" + description: "Database connection idle time before query", + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000]] ), # VM Metrics - summary("vm.memory.total", unit: {:byte, :kilobyte}), - summary("vm.total_run_queue_lengths.total"), - summary("vm.total_run_queue_lengths.cpu"), - summary("vm.total_run_queue_lengths.io") + last_value("vm.memory.total", + unit: {:byte, :kilobyte}, + description: "Total VM memory usage" + ), + last_value("vm.total_run_queue_lengths.total", + description: "Total run queue length" + ), + last_value("vm.total_run_queue_lengths.cpu", + description: "CPU run queue length" + ), + last_value("vm.total_run_queue_lengths.io", + description: "IO run queue length" + ), + + # Application Metrics + counter("ethui.stacks.created.count", + description: "Total number of stacks created" + ), + counter("ethui.stacks.deleted.count", + description: "Total number of stacks deleted" + ), + last_value("ethui.stacks.active.count", + description: "Current number of active stacks" + ), + counter("ethui.api.requests.count", + tags: [:method, :path, :status], + description: "API request count by method, path, and status" + ), + counter("ethui.auth.code_sent.count", + description: "Number of authentication codes sent" + ), + counter("ethui.auth.code_verified.count", + tags: [:status], + description: "Number of authentication verification attempts" + ), + counter("ethui.errors.count", + tags: [:type], + description: "Application errors by type" + ) ] end diff --git a/server/mix.exs b/server/mix.exs index a02fb3d..05e2a19 100644 --- a/server/mix.exs +++ b/server/mix.exs @@ -57,6 +57,8 @@ defmodule Ethui.MixProject do {:finch, "~> 0.13"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, + {:telemetry_metrics_prometheus_core, "~> 1.2"}, + {:logger_json, "~> 6.2"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, {:plug_cowboy, "~> 2.7"}, diff --git a/server/mix.lock b/server/mix.lock index 132f61c..6646dc1 100644 --- a/server/mix.lock +++ b/server/mix.lock @@ -44,6 +44,7 @@ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, + "logger_json": {:hex, :logger_json, "6.2.1", "a1db30e1164e6057f2328a1e4d6b632b9583c015574fdf6c38cf73721128edcb", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "34acd0bfd419d5fcf08c4108a8a4b59b695fcc60409dc1dd1a868b70c42e1d1f"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, @@ -81,6 +82,7 @@ "tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, + "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, "tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"}, "thousand_island": {:hex, :thousand_island, "1.3.12", "590ff651a6d2a59ed7eabea398021749bdc664e2da33e0355e6c64e7e1a2ef93", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "55d0b1c868b513a7225892b8a8af0234d7c8981a51b0740369f3125f7c99a549"}, From 3964670afaf8f5e4e8d49f1fc023611efe91d04d Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 10:48:28 +0000 Subject: [PATCH 02/14] wip --- compose.saas.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compose.saas.yml b/compose.saas.yml index b8d562b..3a81519 100644 --- a/compose.saas.yml +++ b/compose.saas.yml @@ -35,6 +35,11 @@ services: - 'traefik.http.routers.ethui-stacks-secure.tls.domains[0].main=stacks.ethui.dev' - 'traefik.http.routers.ethui-stacks-secure.tls.domains[0].sans=*.stacks.ethui.dev' + # block external access to /metrics + - "traefik.http.routesr.ethui-stacks-secure.middlewares=block-metrics" + - "traefik.http.middlewares.block-metrics.replacepathregex.regex=^/metrics.*" + - "traefik.http.middlewares.block-metrics.replacepathregex.replacement=/404-not-found" + # enabling scraping from alloy/prometheus - "prometheus.scrape=true" - "prometheus.port=4000" From 838781f637854c9eab0968da92b246cb90ec19a2 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 10:59:50 +0000 Subject: [PATCH 03/14] wip --- .../ethui_web/controllers/api/auth_controller.ex | 16 ++++++++-------- .../controllers/api/stack_controller.ex | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/server/lib/ethui_web/controllers/api/auth_controller.ex b/server/lib/ethui_web/controllers/api/auth_controller.ex index 0e4bf96..aa7b6db 100644 --- a/server/lib/ethui_web/controllers/api/auth_controller.ex +++ b/server/lib/ethui_web/controllers/api/auth_controller.ex @@ -12,8 +12,8 @@ defmodule EthuiWeb.Api.AuthController do """ def send_code(conn, %{"email" => email}) do with {:ok, _user} <- Accounts.send_verification_code(email) do - :telemetry.execute( - [:ethui, :auth, :code_sent], + Ethui.Telemetry.exec( + [:auth, :code_sent], %{count: 1}, %{email: email} ) @@ -30,8 +30,8 @@ defmodule EthuiWeb.Api.AuthController do def verify_code(conn, %{"email" => email, "code" => code}) do case Accounts.verify_code_and_generate_token(email, code) do {:ok, token} -> - :telemetry.execute( - [:ethui, :auth, :code_verified], + Ethui.Telemetry.exec( + [:auth, :code_verified], %{count: 1}, %{status: :success, email: email} ) @@ -39,8 +39,8 @@ defmodule EthuiWeb.Api.AuthController do render(conn, :verify_code, token: token) {:error, :invalid_code} -> - :telemetry.execute( - [:ethui, :auth, :code_verified], + Ethui.Telemetry.exec( + [:auth, :code_verified], %{count: 1}, %{status: :invalid_code, email: email} ) @@ -48,8 +48,8 @@ defmodule EthuiWeb.Api.AuthController do {:error, "Invalid or expired verification code"} {:error, _reason} -> - :telemetry.execute( - [:ethui, :auth, :code_verified], + Ethui.Telemetry.exec( + [:auth, :code_verified], %{count: 1}, %{status: :error, email: email} ) diff --git a/server/lib/ethui_web/controllers/api/stack_controller.ex b/server/lib/ethui_web/controllers/api/stack_controller.ex index c6e0320..76e61ed 100644 --- a/server/lib/ethui_web/controllers/api/stack_controller.ex +++ b/server/lib/ethui_web/controllers/api/stack_controller.ex @@ -32,8 +32,8 @@ defmodule EthuiWeb.Api.StackController do with {:ok, stack} <- Stacks.create_stack(user, params), _ <- Server.create(stack) do - :telemetry.execute( - [:ethui, :stacks, :created], + Ethui.Telemetry.exec( + [:stacks, :created], %{count: 1}, %{user_id: user && user.id, stack_slug: stack.slug} ) @@ -51,8 +51,8 @@ defmodule EthuiWeb.Api.StackController do :ok <- authorize_user_access(user, stack), _ <- Server.destroy(stack), {:ok, _} <- Stacks.delete_stack(stack) do - :telemetry.execute( - [:ethui, :stacks, :deleted], + Ethui.Telemetry.exec( + [:stacks, :deleted], %{count: 1}, %{user_id: user && user.id, stack_slug: slug} ) From 3e286282ee629008c282aef51522852a614c76e0 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 10:59:52 +0000 Subject: [PATCH 04/14] wip --- server/lib/ethui/telemetry.ex | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 server/lib/ethui/telemetry.ex diff --git a/server/lib/ethui/telemetry.ex b/server/lib/ethui/telemetry.ex new file mode 100644 index 0000000..8227b8f --- /dev/null +++ b/server/lib/ethui/telemetry.ex @@ -0,0 +1,21 @@ +defmodule Ethui.Telemetry do + @moduledoc """ + Helper module for executing telemetry events with the [:ethui] prefix. + """ + + @doc """ + Execute a telemetry event with the [:ethui] prefix automatically added. + + ## Examples + + Ethui.Telemetry.exec([:stacks, :created], %{count: 1}, %{stack_slug: "demo"}) + # Executes: :telemetry.execute([:ethui, :stacks, :created], %{count: 1}, %{stack_slug: "demo"}) + + Ethui.Telemetry.exec([:auth, :code_sent], %{count: 1}, %{email: "user@example.com"}) + # Executes: :telemetry.execute([:ethui, :auth, :code_sent], %{count: 1}, %{email: "user@example.com"}) + """ + @spec exec(event :: [atom()], measurements :: map(), metadata :: map()) :: :ok + def exec(event, measurements \\ %{}, metadata \\ %{}) when is_list(event) do + :telemetry.execute([:ethui | event], measurements, metadata) + end +end From 8fecfb8212bd45a0b12d792b45d890ad783eed1b Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 11:01:12 +0000 Subject: [PATCH 05/14] wip --- server/lib/ethui/telemetry.ex | 11 ++++++----- .../lib/ethui_web/controllers/api/auth_controller.ex | 4 ---- .../lib/ethui_web/controllers/api/stack_controller.ex | 2 -- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/server/lib/ethui/telemetry.ex b/server/lib/ethui/telemetry.ex index 8227b8f..f72ef41 100644 --- a/server/lib/ethui/telemetry.ex +++ b/server/lib/ethui/telemetry.ex @@ -5,17 +5,18 @@ defmodule Ethui.Telemetry do @doc """ Execute a telemetry event with the [:ethui] prefix automatically added. + Measurements are automatically set to %{count: 1}. ## Examples - Ethui.Telemetry.exec([:stacks, :created], %{count: 1}, %{stack_slug: "demo"}) + Ethui.Telemetry.exec([:stacks, :created], %{stack_slug: "demo"}) # Executes: :telemetry.execute([:ethui, :stacks, :created], %{count: 1}, %{stack_slug: "demo"}) - Ethui.Telemetry.exec([:auth, :code_sent], %{count: 1}, %{email: "user@example.com"}) + Ethui.Telemetry.exec([:auth, :code_sent], %{email: "user@example.com"}) # Executes: :telemetry.execute([:ethui, :auth, :code_sent], %{count: 1}, %{email: "user@example.com"}) """ - @spec exec(event :: [atom()], measurements :: map(), metadata :: map()) :: :ok - def exec(event, measurements \\ %{}, metadata \\ %{}) when is_list(event) do - :telemetry.execute([:ethui | event], measurements, metadata) + @spec exec(event :: [atom()], metadata :: map()) :: :ok + def exec(event, metadata \\ %{}) when is_list(event) do + :telemetry.execute([:ethui | event], %{count: 1}, metadata) end end diff --git a/server/lib/ethui_web/controllers/api/auth_controller.ex b/server/lib/ethui_web/controllers/api/auth_controller.ex index aa7b6db..a37b4cb 100644 --- a/server/lib/ethui_web/controllers/api/auth_controller.ex +++ b/server/lib/ethui_web/controllers/api/auth_controller.ex @@ -14,7 +14,6 @@ defmodule EthuiWeb.Api.AuthController do with {:ok, _user} <- Accounts.send_verification_code(email) do Ethui.Telemetry.exec( [:auth, :code_sent], - %{count: 1}, %{email: email} ) @@ -32,7 +31,6 @@ defmodule EthuiWeb.Api.AuthController do {:ok, token} -> Ethui.Telemetry.exec( [:auth, :code_verified], - %{count: 1}, %{status: :success, email: email} ) @@ -41,7 +39,6 @@ defmodule EthuiWeb.Api.AuthController do {:error, :invalid_code} -> Ethui.Telemetry.exec( [:auth, :code_verified], - %{count: 1}, %{status: :invalid_code, email: email} ) @@ -50,7 +47,6 @@ defmodule EthuiWeb.Api.AuthController do {:error, _reason} -> Ethui.Telemetry.exec( [:auth, :code_verified], - %{count: 1}, %{status: :error, email: email} ) diff --git a/server/lib/ethui_web/controllers/api/stack_controller.ex b/server/lib/ethui_web/controllers/api/stack_controller.ex index 76e61ed..5ae03b7 100644 --- a/server/lib/ethui_web/controllers/api/stack_controller.ex +++ b/server/lib/ethui_web/controllers/api/stack_controller.ex @@ -34,7 +34,6 @@ defmodule EthuiWeb.Api.StackController do _ <- Server.create(stack) do Ethui.Telemetry.exec( [:stacks, :created], - %{count: 1}, %{user_id: user && user.id, stack_slug: stack.slug} ) @@ -53,7 +52,6 @@ defmodule EthuiWeb.Api.StackController do {:ok, _} <- Stacks.delete_stack(stack) do Ethui.Telemetry.exec( [:stacks, :deleted], - %{count: 1}, %{user_id: user && user.id, stack_slug: slug} ) From 7ad1a96a3082585ea892681dc28228fe3b42f715 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 11:01:57 +0000 Subject: [PATCH 06/14] wip --- server/lib/ethui/telemetry.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/ethui/telemetry.ex b/server/lib/ethui/telemetry.ex index f72ef41..f6a8252 100644 --- a/server/lib/ethui/telemetry.ex +++ b/server/lib/ethui/telemetry.ex @@ -17,6 +17,6 @@ defmodule Ethui.Telemetry do """ @spec exec(event :: [atom()], metadata :: map()) :: :ok def exec(event, metadata \\ %{}) when is_list(event) do - :telemetry.execute([:ethui | event], %{count: 1}, metadata) + :telemetry.execute([:ethui_stacks | event], %{count: 1}, metadata) end end From 3170e80779758396f0bc177c9b34fb48e14048a9 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 11:02:40 +0000 Subject: [PATCH 07/14] wip --- server/lib/ethui/telemetry.ex | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/server/lib/ethui/telemetry.ex b/server/lib/ethui/telemetry.ex index f6a8252..f3cd646 100644 --- a/server/lib/ethui/telemetry.ex +++ b/server/lib/ethui/telemetry.ex @@ -1,20 +1,8 @@ defmodule Ethui.Telemetry do @moduledoc """ - Helper module for executing telemetry events with the [:ethui] prefix. + Helper module for executing telemetry events with the [:ethui_stacks] prefix. """ - @doc """ - Execute a telemetry event with the [:ethui] prefix automatically added. - Measurements are automatically set to %{count: 1}. - - ## Examples - - Ethui.Telemetry.exec([:stacks, :created], %{stack_slug: "demo"}) - # Executes: :telemetry.execute([:ethui, :stacks, :created], %{count: 1}, %{stack_slug: "demo"}) - - Ethui.Telemetry.exec([:auth, :code_sent], %{email: "user@example.com"}) - # Executes: :telemetry.execute([:ethui, :auth, :code_sent], %{count: 1}, %{email: "user@example.com"}) - """ @spec exec(event :: [atom()], metadata :: map()) :: :ok def exec(event, metadata \\ %{}) when is_list(event) do :telemetry.execute([:ethui_stacks | event], %{count: 1}, metadata) From b1a50261d4c48c343f00f3d77faaba1da41c165a Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 11:03:42 +0000 Subject: [PATCH 08/14] wip --- .../controllers/api/auth_controller.ex | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/server/lib/ethui_web/controllers/api/auth_controller.ex b/server/lib/ethui_web/controllers/api/auth_controller.ex index a37b4cb..d4a0e96 100644 --- a/server/lib/ethui_web/controllers/api/auth_controller.ex +++ b/server/lib/ethui_web/controllers/api/auth_controller.ex @@ -12,10 +12,7 @@ defmodule EthuiWeb.Api.AuthController do """ def send_code(conn, %{"email" => email}) do with {:ok, _user} <- Accounts.send_verification_code(email) do - Ethui.Telemetry.exec( - [:auth, :code_sent], - %{email: email} - ) + Ethui.Telemetry.exec([:auth, :code_sent], %{email: email}) render(conn, :send_code, message: "Verification code sent") end @@ -29,26 +26,17 @@ defmodule EthuiWeb.Api.AuthController do def verify_code(conn, %{"email" => email, "code" => code}) do case Accounts.verify_code_and_generate_token(email, code) do {:ok, token} -> - Ethui.Telemetry.exec( - [:auth, :code_verified], - %{status: :success, email: email} - ) + Ethui.Telemetry.exec([:auth, :code_verified], %{status: :success, email: email}) render(conn, :verify_code, token: token) {:error, :invalid_code} -> - Ethui.Telemetry.exec( - [:auth, :code_verified], - %{status: :invalid_code, email: email} - ) + Ethui.Telemetry.exec([:auth, :code_verified], %{status: :invalid_code, email: email}) {:error, "Invalid or expired verification code"} {:error, _reason} -> - Ethui.Telemetry.exec( - [:auth, :code_verified], - %{status: :error, email: email} - ) + Ethui.Telemetry.exec([:auth, :code_verified], %{status: :error, email: email}) {:error, "Verification failed"} end From a8ceda9f180977129dfe04674c6816bab9acc73c Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 11:04:07 +0000 Subject: [PATCH 09/14] wip --- .../lib/ethui_web/controllers/api/stack_controller.ex | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/server/lib/ethui_web/controllers/api/stack_controller.ex b/server/lib/ethui_web/controllers/api/stack_controller.ex index 5ae03b7..d65d5fe 100644 --- a/server/lib/ethui_web/controllers/api/stack_controller.ex +++ b/server/lib/ethui_web/controllers/api/stack_controller.ex @@ -32,10 +32,7 @@ defmodule EthuiWeb.Api.StackController do with {:ok, stack} <- Stacks.create_stack(user, params), _ <- Server.create(stack) do - Ethui.Telemetry.exec( - [:stacks, :created], - %{user_id: user && user.id, stack_slug: stack.slug} - ) + Ethui.Telemetry.exec([:stacks, :created], %{user_id: user && user.id, stack_slug: stack.slug}) conn |> put_status(:created) @@ -50,10 +47,7 @@ defmodule EthuiWeb.Api.StackController do :ok <- authorize_user_access(user, stack), _ <- Server.destroy(stack), {:ok, _} <- Stacks.delete_stack(stack) do - Ethui.Telemetry.exec( - [:stacks, :deleted], - %{user_id: user && user.id, stack_slug: slug} - ) + Ethui.Telemetry.exec([:stacks, :deleted], %{user_id: user && user.id, stack_slug: slug}) send_resp(conn, :no_content, "") else From 934c3a0d06ded8cf0c103b7fcb9734e4c0251fa2 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 13:21:11 +0000 Subject: [PATCH 10/14] wip --- server/lib/ethui/telemetry.ex | 4 +-- .../controllers/api/auth_controller.ex | 8 ++--- .../controllers/metrics_controller.ex | 4 --- server/lib/ethui_web/plugs/log_metadata.ex | 13 ++++++++ server/lib/ethui_web/telemetry.ex | 31 +++++++++++++------ 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/server/lib/ethui/telemetry.ex b/server/lib/ethui/telemetry.ex index f3cd646..a3373f0 100644 --- a/server/lib/ethui/telemetry.ex +++ b/server/lib/ethui/telemetry.ex @@ -1,10 +1,10 @@ defmodule Ethui.Telemetry do @moduledoc """ - Helper module for executing telemetry events with the [:ethui_stacks] prefix. + Helper module for executing telemetry events with the [:ethui] prefix. """ @spec exec(event :: [atom()], metadata :: map()) :: :ok def exec(event, metadata \\ %{}) when is_list(event) do - :telemetry.execute([:ethui_stacks | event], %{count: 1}, metadata) + :telemetry.execute([:ethui | event], %{count: 1}, metadata) end end diff --git a/server/lib/ethui_web/controllers/api/auth_controller.ex b/server/lib/ethui_web/controllers/api/auth_controller.ex index d4a0e96..31f921f 100644 --- a/server/lib/ethui_web/controllers/api/auth_controller.ex +++ b/server/lib/ethui_web/controllers/api/auth_controller.ex @@ -12,7 +12,7 @@ defmodule EthuiWeb.Api.AuthController do """ def send_code(conn, %{"email" => email}) do with {:ok, _user} <- Accounts.send_verification_code(email) do - Ethui.Telemetry.exec([:auth, :code_sent], %{email: email}) + Ethui.Telemetry.exec([:auth, :code_sent]) render(conn, :send_code, message: "Verification code sent") end @@ -26,17 +26,17 @@ defmodule EthuiWeb.Api.AuthController do def verify_code(conn, %{"email" => email, "code" => code}) do case Accounts.verify_code_and_generate_token(email, code) do {:ok, token} -> - Ethui.Telemetry.exec([:auth, :code_verified], %{status: :success, email: email}) + Ethui.Telemetry.exec([:auth, :code_verified], %{status: :success}) render(conn, :verify_code, token: token) {:error, :invalid_code} -> - Ethui.Telemetry.exec([:auth, :code_verified], %{status: :invalid_code, email: email}) + Ethui.Telemetry.exec([:auth, :code_verified], %{status: :invalid_code}) {:error, "Invalid or expired verification code"} {:error, _reason} -> - Ethui.Telemetry.exec([:auth, :code_verified], %{status: :error, email: email}) + Ethui.Telemetry.exec([:auth, :code_verified], %{status: :error}) {:error, "Verification failed"} end diff --git a/server/lib/ethui_web/controllers/metrics_controller.ex b/server/lib/ethui_web/controllers/metrics_controller.ex index 0315d32..a619e6f 100644 --- a/server/lib/ethui_web/controllers/metrics_controller.ex +++ b/server/lib/ethui_web/controllers/metrics_controller.ex @@ -3,10 +3,6 @@ defmodule EthuiWeb.MetricsController do @moduledoc """ Exposes Prometheus metrics for scraping. - - This endpoint is designed to be accessible only within the internal Docker/Dokploy network, - not via external Traefik routing. Prometheus should scrape metrics directly from the - application on port 4000. """ def index(conn, _params) do diff --git a/server/lib/ethui_web/plugs/log_metadata.ex b/server/lib/ethui_web/plugs/log_metadata.ex index 342c7cd..d69ae8d 100644 --- a/server/lib/ethui_web/plugs/log_metadata.ex +++ b/server/lib/ethui_web/plugs/log_metadata.ex @@ -40,6 +40,13 @@ defmodule EthuiWeb.Plugs.LogMetadata do status: conn.status, duration: calculate_duration(conn) ) + + # Emit API request telemetry event + Ethui.Telemetry.exec( + [:api, :requests], + %{method: conn.method, path: conn.request_path, status: conn.status} + ) + conn end) end @@ -52,6 +59,12 @@ defmodule EthuiWeb.Plugs.LogMetadata do end defp format_ip({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}" + + defp format_ip({a, b, c, d, e, f, g, h}) do + # IPv6 address - convert to standard colon-separated hex notation + :inet.ntoa({a, b, c, d, e, f, g, h}) |> to_string() + end + defp format_ip(ip), do: inspect(ip) defp calculate_duration(conn) do diff --git a/server/lib/ethui_web/telemetry.ex b/server/lib/ethui_web/telemetry.ex index 310e30a..c130cb4 100644 --- a/server/lib/ethui_web/telemetry.ex +++ b/server/lib/ethui_web/telemetry.ex @@ -102,27 +102,27 @@ defmodule EthuiWeb.Telemetry do ), # Application Metrics - counter("ethui.stacks.created.count", + counter("ethui.stacks.created", description: "Total number of stacks created" ), - counter("ethui.stacks.deleted.count", + counter("ethui.stacks.deleted", description: "Total number of stacks deleted" ), - last_value("ethui.stacks.active.count", + last_value("ethui.stacks.active", description: "Current number of active stacks" ), - counter("ethui.api.requests.count", + counter("ethui.api.requests", tags: [:method, :path, :status], description: "API request count by method, path, and status" ), - counter("ethui.auth.code_sent.count", + counter("ethui.auth.code_sent", description: "Number of authentication codes sent" ), - counter("ethui.auth.code_verified.count", + counter("ethui.auth.code_verified", tags: [:status], description: "Number of authentication verification attempts" ), - counter("ethui.errors.count", + counter("ethui.errors", tags: [:type], description: "Application errors by type" ) @@ -131,9 +131,20 @@ defmodule EthuiWeb.Telemetry do defp periodic_measurements do [ - # A module, function and arguments to be invoked periodically. - # This function must call :telemetry.execute/3 and a metric must be added above. - # {EthuiWeb, :count_users, []} + {__MODULE__, :publish_active_stacks_count, []} ] end + + @doc """ + Periodically called to publish the current count of active stacks. + """ + def publish_active_stacks_count do + import Ecto.Query, only: [from: 2] + alias Ethui.Stacks.Stack + alias Ethui.Repo + + count = from(s in Stack, select: count(s.id)) |> Repo.one() + + :telemetry.execute([:ethui, :stacks, :active], %{count: count}, %{}) + end end From a309f8f97245aed18a5c156d76f676961a9eebc5 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 13:45:47 +0000 Subject: [PATCH 11/14] wip --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87856bc..d44454f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,9 @@ jobs: path: | server/priv/plts + - name: Install dependencies + run: mix deps.get + # Create PLTs if no cache was found - name: Create PLTs if: steps.plt_cache.outputs.cache-hit != 'true' @@ -69,8 +72,6 @@ jobs: path: | server/priv/plts - - name: Install dependencies - run: mix deps.get - name: Compile run: mix compile - name: Create database From bc6d8ce4439bcd0da7a7099be102ab66ddef6c00 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 13:58:09 +0000 Subject: [PATCH 12/14] wip --- .../ethui_web/controllers/fallback_controller.ex | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/lib/ethui_web/controllers/fallback_controller.ex b/server/lib/ethui_web/controllers/fallback_controller.ex index 37f442c..cba0c83 100644 --- a/server/lib/ethui_web/controllers/fallback_controller.ex +++ b/server/lib/ethui_web/controllers/fallback_controller.ex @@ -7,8 +7,12 @@ defmodule EthuiWeb.FallbackController do use EthuiWeb, :controller + alias Ethui.Telemetry + # Handles Ecto changeset errors. def call(conn, {:error, %Ecto.Changeset{} = changeset}) do + Telemetry.exec([:errors], %{type: :changeset_error}) + conn |> put_status(:unprocessable_entity) |> put_view(json: EthuiWeb.ChangesetJSON) @@ -17,6 +21,8 @@ defmodule EthuiWeb.FallbackController do # Handles not found errors. def call(conn, {:error, :not_found}) do + Telemetry.exec([:errors], %{type: :not_found}) + conn |> put_status(:not_found) |> put_view(json: EthuiWeb.ErrorJSON) @@ -25,6 +31,8 @@ defmodule EthuiWeb.FallbackController do # Handles unauthorized access errors. def call(conn, {:error, :unauthorized}) do + Telemetry.exec([:errors], %{type: :unauthorized}) + conn |> put_status(:forbidden) |> put_view(json: EthuiWeb.ErrorJSON) @@ -32,6 +40,8 @@ defmodule EthuiWeb.FallbackController do end def call(conn, {:error, {:user_limit_exceeded, limit}}) do + Telemetry.exec([:errors], %{type: :user_limit_exceeded}) + conn |> put_status(:forbidden) |> put_view(json: EthuiWeb.ErrorJSON) @@ -41,6 +51,8 @@ defmodule EthuiWeb.FallbackController do end def call(conn, {:error, {:global_limit_exceeded, limit}}) do + Telemetry.exec([:errors], %{type: :global_limit_exceeded}) + conn |> put_status(:service_unavailable) |> put_view(json: EthuiWeb.ErrorJSON) @@ -51,6 +63,8 @@ defmodule EthuiWeb.FallbackController do # Handles generic errors. def call(conn, {:error, reason}) when is_binary(reason) do + Telemetry.exec([:errors], %{type: :validation_error}) + conn |> put_status(:unprocessable_entity) |> put_view(json: EthuiWeb.ErrorJSON) @@ -59,6 +73,8 @@ defmodule EthuiWeb.FallbackController do # Handles unexpected errors. def call(conn, _error) do + Telemetry.exec([:errors], %{type: :internal_server_error}) + conn |> put_status(:internal_server_error) |> put_view(json: EthuiWeb.ErrorJSON) From c447783620a9f1100a44a1b7e72373bab006a58e Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 14:26:51 +0000 Subject: [PATCH 13/14] wip --- server/config/config.exs | 2 +- server/lib/ethui_web/telemetry.ex | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/config/config.exs b/server/config/config.exs index b7de583..8e09098 100644 --- a/server/config/config.exs +++ b/server/config/config.exs @@ -73,7 +73,7 @@ config :tailwind, # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", - metadata: [:request_id] + metadata: [:request_id, :remote_ip, :method, :path, :user_id, :stack_slug, :status, :duration] # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason diff --git a/server/lib/ethui_web/telemetry.ex b/server/lib/ethui_web/telemetry.ex index c130cb4..6f9bd3c 100644 --- a/server/lib/ethui_web/telemetry.ex +++ b/server/lib/ethui_web/telemetry.ex @@ -25,19 +25,19 @@ defmodule EthuiWeb.Telemetry do distribution("phoenix.endpoint.stop.duration", unit: {:native, :millisecond}, description: "Phoenix endpoint response time", - reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000]] + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10_000]] ), distribution("phoenix.router_dispatch.stop.duration", tags: [:route], unit: {:native, :millisecond}, description: "Phoenix router dispatch time by route", - reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000]] + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10_000]] ), distribution("phoenix.router_dispatch.exception.duration", tags: [:route], unit: {:native, :millisecond}, description: "Phoenix router exception duration", - reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000]] + reporter_options: [buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10_000]] ), distribution("phoenix.socket_connected.duration", unit: {:native, :millisecond}, From 9acf22e660cb5f4e1c1c9f7c4f2241bc9b4da75c Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 3 Feb 2026 14:45:57 +0000 Subject: [PATCH 14/14] wip --- server/lib/ethui_web/plugs/log_metadata.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/lib/ethui_web/plugs/log_metadata.ex b/server/lib/ethui_web/plugs/log_metadata.ex index d69ae8d..3348867 100644 --- a/server/lib/ethui_web/plugs/log_metadata.ex +++ b/server/lib/ethui_web/plugs/log_metadata.ex @@ -65,8 +65,6 @@ defmodule EthuiWeb.Plugs.LogMetadata do :inet.ntoa({a, b, c, d, e, f, g, h}) |> to_string() end - defp format_ip(ip), do: inspect(ip) - defp calculate_duration(conn) do case conn.private[:phoenix_endpoint_start] do %{system: start} ->