defmodule ToweropsWeb.Telemetry do @moduledoc false use Supervisor import Telemetry.Metrics def start_link(arg) do Supervisor.start_link(__MODULE__, arg, name: __MODULE__) end @impl true def init(_arg) do # Attach telemetry handlers for logging request failures :telemetry.attach( "towerops-router-exception", [:phoenix, :router_dispatch, :exception], &__MODULE__.handle_router_exception/4, nil ) :telemetry.attach( "towerops-endpoint-stop", [:phoenix, :endpoint, :stop], &__MODULE__.handle_endpoint_stop/4, nil ) 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()} ] Supervisor.init(children, strategy: :one_for_one) end def metrics do [ # Phoenix Metrics summary("phoenix.endpoint.start.system_time", unit: {:native, :millisecond} ), summary("phoenix.endpoint.stop.duration", unit: {:native, :millisecond} ), summary("phoenix.router_dispatch.start.system_time", tags: [:route], unit: {:native, :millisecond} ), summary("phoenix.router_dispatch.exception.duration", tags: [:route], unit: {:native, :millisecond} ), summary("phoenix.router_dispatch.stop.duration", tags: [:route], unit: {:native, :millisecond} ), summary("phoenix.socket_connected.duration", unit: {:native, :millisecond} ), sum("phoenix.socket_drain.count"), summary("phoenix.channel_joined.duration", unit: {:native, :millisecond} ), summary("phoenix.channel_handled_in.duration", tags: [:event], unit: {:native, :millisecond} ), # Database Metrics summary("towerops.repo.query.total_time", unit: {:native, :millisecond}, description: "The sum of the other measurements" ), summary("towerops.repo.query.decode_time", unit: {:native, :millisecond}, description: "The time spent decoding the data received from the database" ), summary("towerops.repo.query.query_time", unit: {:native, :millisecond}, description: "The time spent executing the query" ), summary("towerops.repo.query.queue_time", unit: {:native, :millisecond}, description: "The time spent waiting for a database connection" ), summary("towerops.repo.query.idle_time", unit: {:native, :millisecond}, description: "The time the connection spent waiting before being checked out for the query" ), # 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") ] end 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. # {ToweropsWeb, :count_users, []} ] end # Telemetry handler for router exceptions def handle_router_exception(_event, _measurements, metadata, _config) do require Logger Logger.error( "Router exception on #{metadata.plug} #{metadata.conn.method} #{metadata.conn.request_path}", kind: metadata.kind, reason: metadata.reason, stacktrace: metadata.stacktrace, request_id: metadata.conn.assigns[:request_id] ) end # Telemetry handler for endpoint stop events (log slow requests and errors) def handle_endpoint_stop(_event, measurements, metadata, _config) do require Logger duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond) # Log slow requests (over 5 seconds) if duration_ms > 5_000 do Logger.warning( "Slow request: #{metadata.conn.method} #{metadata.conn.request_path} took #{duration_ms}ms", request_id: metadata.conn.assigns[:request_id], duration_ms: duration_ms ) end # Log requests with non-2xx status codes status = metadata.conn.status if status >= 500 do Logger.error( "Server error: #{metadata.conn.method} #{metadata.conn.request_path} returned #{status}", request_id: metadata.conn.assigns[:request_id], status: status, duration_ms: duration_ms ) end end end