feat: Chain & explorer Prometheus metrics (#10063)

* feat: Chain & explorer Prometheus metrics

* Process review

* Add consensus filter, account token transfers and internal transactions in active users metric

* Refactor, run metrics async

* Raise error on metrics query execution timed out

* Move chain & explorer mterics to a separate (public) endpoint

* Set 1h timeout for DB request
pull/10261/head
Victor Baranov 5 months ago committed by GitHub
parent 454edb0d2e
commit 4f0cc81fa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      apps/block_scout_web/config/config.exs
  2. 8
      apps/block_scout_web/lib/block_scout_web/application.ex
  3. 1
      apps/block_scout_web/lib/block_scout_web/endpoint.ex
  4. 9
      apps/block_scout_web/lib/block_scout_web/prometheus/public_exporter.ex
  5. 80
      apps/explorer/lib/explorer/chain/metrics.ex
  6. 246
      apps/explorer/lib/explorer/chain/metrics/queries.ex
  7. 70
      apps/explorer/lib/explorer/prometheus/instrumenter.ex
  8. 2
      apps/explorer/mix.exs
  9. 4
      mix.lock

@ -75,6 +75,12 @@ config :logger, :api_v2,
block_number step count error_count shrunk import_id transaction_id)a,
metadata_filter: [application: :api_v2]
config :prometheus, BlockScoutWeb.Prometheus.PublicExporter,
path: "/public-metrics",
format: :auto,
registry: :public,
auth: false
config :prometheus, BlockScoutWeb.Prometheus.PhoenixInstrumenter,
# override default for Phoenix 1.4 compatibility
# * `:transport_name` to `:transport`

@ -6,12 +6,16 @@ defmodule BlockScoutWeb.Application do
use Application
alias BlockScoutWeb.Endpoint
alias BlockScoutWeb.Prometheus.Exporter, as: PrometheusExporter
alias BlockScoutWeb.Prometheus.PublicExporter, as: PrometheusPublicExporter
def start(_type, _args) do
base_children = [Supervisor.child_spec(Endpoint, [])]
api_children = setup_and_define_children()
all_children = base_children ++ api_children
opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000]
PrometheusExporter.setup()
PrometheusPublicExporter.setup()
Supervisor.start_link(all_children, opts)
end
@ -31,6 +35,7 @@ defmodule BlockScoutWeb.Application do
alias BlockScoutWeb.Prometheus.{Exporter, PhoenixInstrumenter}
alias BlockScoutWeb.{MainPageRealtimeEventHandler, RealtimeEventHandler, SmartContractRealtimeEventHandler}
alias BlockScoutWeb.Utility.EventHandlersMetrics
alias Explorer.Chain.Metrics, as: ChainMetrics
PhoenixInstrumenter.setup()
Exporter.setup()
@ -57,7 +62,8 @@ defmodule BlockScoutWeb.Application do
{SmartContractRealtimeEventHandler, name: SmartContractRealtimeEventHandler},
{BlocksIndexedCounter, name: BlocksIndexedCounter},
{InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter},
{EventHandlersMetrics, []}
{EventHandlersMetrics, []},
{ChainMetrics, []}
]
end
end

@ -67,6 +67,7 @@ defmodule BlockScoutWeb.Endpoint do
use SpandexPhoenix
plug(BlockScoutWeb.Prometheus.Exporter)
plug(BlockScoutWeb.Prometheus.PublicExporter)
# 'x-apollo-tracing' header for https://www.graphqlbin.com to work with our GraphQL endpoint
plug(CORSPlug, headers: ["x-apollo-tracing" | CORSPlug.defaults()[:headers]])

@ -0,0 +1,9 @@
defmodule BlockScoutWeb.Prometheus.PublicExporter do
@moduledoc """
Exports `Prometheus` metrics at `/public-metrics`
"""
@dialyzer :no_match
use Prometheus.PlugExporter
end

@ -0,0 +1,80 @@
defmodule Explorer.Chain.Metrics do
@moduledoc """
Module responsible for periodically setting current chain metrics.
"""
use GenServer
import Explorer.Chain, only: [select_repo: 1]
alias Explorer.Chain.Metrics.Queries
alias Explorer.Prometheus.Instrumenter
@interval :timer.hours(1)
@options [timeout: 60_000, api?: true]
@metrics_list [
:weekly_success_transactions_number,
:weekly_deployed_smart_contracts_number,
:weekly_verified_smart_contracts_number,
:weekly_new_addresses_number,
:weekly_new_tokens_number,
:weekly_new_token_transfers_number,
:weekly_active_addresses_number
]
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(_) do
send(self(), :set_metrics)
{:ok, %{}}
end
def handle_info(:set_metrics, state) do
schedule_next_run()
set_metrics()
{:noreply, state}
end
defp set_metrics do
@metrics_list
|> Enum.map(fn metric ->
Task.async(fn ->
set_handler_metric(metric)
end)
end)
|> Task.yield_many(:timer.hours(1))
|> Enum.map(fn {_task, res} ->
case res do
{:ok, result} ->
result
{:exit, reason} ->
raise "Query fetching explorer & chain metrics terminated: #{inspect(reason)}"
nil ->
raise "Query fetching explorer & chain metrics timed out."
end
end)
end
# sobelow_skip ["DOS.StringToAtom"]
defp set_handler_metric(metric) do
func = String.to_atom(to_string(metric) <> "_query")
weekly_transactions_count =
Queries
|> apply(func, [])
|> select_repo(@options).one()
apply(Instrumenter, metric, [weekly_transactions_count])
end
defp schedule_next_run do
Process.send_after(self(), :set_metrics, @interval)
end
end

@ -0,0 +1,246 @@
defmodule Explorer.Chain.Metrics.Queries do
@moduledoc """
Module for DB queries to get chain metrics exposed at /metrics endpoint
"""
import Ecto.Query,
only: [
distinct: 2,
from: 2,
join: 4,
select: 3,
subquery: 1,
union: 2,
where: 3
]
import Explorer.Chain, only: [wrapped_union_subquery: 1]
alias Explorer.Chain.{
Address,
DenormalizationHelper,
InternalTransaction,
SmartContract,
Token,
TokenTransfer,
Transaction
}
@doc """
Retrieves the query for fetching the number of successful transactions in a week.
"""
@spec weekly_success_transactions_number_query() :: Ecto.Query.t()
def weekly_success_transactions_number_query do
if DenormalizationHelper.transactions_denormalization_finished?() do
Transaction
|> where([tx], tx.block_timestamp >= ago(7, "day"))
|> where([tx], tx.block_consensus == true)
|> where([tx], tx.status == ^1)
|> select([tx], count(tx.hash))
else
Transaction
|> join(:inner, [tx], block in assoc(tx, :block))
|> where([tx, block], block.timestamp >= ago(7, "day"))
|> where([tx, block], block.consensus == true)
|> where([tx, block], tx.status == ^1)
|> select([tx, block], count(tx.hash))
end
end
@doc """
Retrieves the query for the number of smart contracts deployed in the current week.
"""
@spec weekly_deployed_smart_contracts_number_query() :: Ecto.Query.t()
def weekly_deployed_smart_contracts_number_query do
transactions_query =
if DenormalizationHelper.transactions_denormalization_finished?() do
Transaction
|> where([tx], not is_nil(tx.created_contract_address_hash))
|> where([tx], tx.block_timestamp >= ago(7, "day"))
|> where([tx], tx.block_consensus == true)
|> where([tx], tx.status == ^1)
|> select([tx], tx.created_contract_address_hash)
else
Transaction
|> join(:inner, [tx], block in assoc(tx, :block))
|> where([tx], not is_nil(tx.created_contract_address_hash))
|> where([tx, block], block.consensus == true)
|> where([tx, block], block.timestamp >= ago(7, "day"))
|> where([tx, block], tx.status == ^1)
|> select([tx, block], tx.created_contract_address_hash)
end
internal_transactions_query =
InternalTransaction
|> join(:inner, [it], transaction in assoc(it, :transaction))
|> where([it, tx], not is_nil(it.created_contract_address_hash))
|> where([it, tx], tx.block_timestamp >= ago(7, "day"))
|> where([it, tx], tx.block_consensus == true)
|> where([it, tx], tx.status == ^1)
|> select([it, tx], it.created_contract_address_hash)
|> wrapped_union_subquery()
query =
transactions_query
|> wrapped_union_subquery()
|> union(^internal_transactions_query)
from(
q in subquery(query),
select: fragment("COUNT(DISTINCT(?))", q.created_contract_address_hash)
)
end
@doc """
Retrieves the query for the number of verified smart contracts in the current week.
"""
@spec weekly_verified_smart_contracts_number_query() :: Ecto.Query.t()
def weekly_verified_smart_contracts_number_query do
SmartContract
|> where([sc], sc.inserted_at >= ago(7, "day"))
|> select([sc], count(sc.address_hash))
end
@doc """
Retrieves the query for the number of new addresses in the current week.
"""
@spec weekly_new_addresses_number_query() :: Ecto.Query.t()
def weekly_new_addresses_number_query do
Address
|> where([a], a.inserted_at >= ago(7, "day"))
|> select([a], count(a.hash))
end
@doc """
Retrieves the query for the number of new tokens detected in the current week.
"""
@spec weekly_new_tokens_number_query() :: Ecto.Query.t()
def weekly_new_tokens_number_query do
Token
|> where([token], token.inserted_at >= ago(7, "day"))
|> select([token], count(token.contract_address_hash))
end
@doc """
Retrieves the query for the number of new token transfers detected in the current week.
"""
@spec weekly_new_token_transfers_number_query() :: Ecto.Query.t()
def weekly_new_token_transfers_number_query do
TokenTransfer
|> join(:inner, [tt], block in assoc(tt, :block))
|> where([tt, block], block.timestamp >= ago(7, "day"))
|> where([tt, block], block.consensus == true)
|> select([tt, block], fragment("COUNT(*)"))
end
@doc """
Retrieves the query for the number of active EOA and smart-contract addresses in the current week.
"""
@spec weekly_active_addresses_number_query() :: Ecto.Query.t()
def weekly_active_addresses_number_query do
transactions_query =
if DenormalizationHelper.transactions_denormalization_finished?() do
Transaction
|> where([tx], tx.block_timestamp >= ago(7, "day"))
|> where([tx], tx.block_consensus == true)
|> distinct(true)
|> select([tx], %{
address_hash:
fragment(
"UNNEST(ARRAY[?, ?, ?])",
tx.from_address_hash,
tx.to_address_hash,
tx.created_contract_address_hash
)
})
else
Transaction
|> join(:inner, [tx], block in assoc(tx, :block))
|> where([tx, block], block.timestamp >= ago(7, "day"))
|> where([tx, block], block.consensus == true)
|> distinct(true)
|> select([tx, block], %{
address_hash:
fragment(
"UNNEST(ARRAY[?, ?, ?])",
tx.from_address_hash,
tx.to_address_hash,
tx.created_contract_address_hash
)
})
end
internal_transactions_query =
if DenormalizationHelper.transactions_denormalization_finished?() do
InternalTransaction
|> join(:inner, [it], transaction in assoc(it, :transaction))
|> where([it, tx], tx.block_timestamp >= ago(7, "day"))
|> where([it, tx], tx.block_consensus == true)
|> where([it, tx], tx.status == ^1)
|> select([it, tx], %{
address_hash:
fragment(
"UNNEST(ARRAY[?, ?, ?])",
it.from_address_hash,
it.to_address_hash,
it.created_contract_address_hash
)
})
|> wrapped_union_subquery()
else
InternalTransaction
|> join(:inner, [it], transaction in assoc(it, :transaction))
|> join(:inner, [tx], block in assoc(tx, :block))
|> where([it, tx, block], tx.block_timestamp >= ago(7, "day"))
|> where([it, tx, block], block.consensus == true)
|> where([it, tx, block], tx.status == ^1)
|> select([it, tx, block], %{
address_hash:
fragment(
"UNNEST(ARRAY[?, ?, ?])",
it.from_address_hash,
it.to_address_hash,
it.created_contract_address_hash
)
})
|> wrapped_union_subquery()
end
token_transfers_query =
if DenormalizationHelper.transactions_denormalization_finished?() do
TokenTransfer
|> join(:inner, [tt], transaction in assoc(tt, :transaction))
|> where([tt, tx], tx.block_timestamp >= ago(7, "day"))
|> where([tt, tx], tx.block_consensus == true)
|> where([tt, tx], tx.status == ^1)
|> select([tt, tx], %{
address_hash:
fragment("UNNEST(ARRAY[?, ?, ?])", tt.from_address_hash, tt.to_address_hash, tt.token_contract_address_hash)
})
|> wrapped_union_subquery()
else
TokenTransfer
|> join(:inner, [tt], transaction in assoc(tt, :transaction))
|> join(:inner, [tx], block in assoc(tx, :block))
|> where([tt, tx, block], tx.block_timestamp >= ago(7, "day"))
|> where([tt, tx, block], block.consensus == true)
|> where([tt, tx, block], tx.status == ^1)
|> select([tt, tx, block], %{
address_hash:
fragment("UNNEST(ARRAY[?, ?, ?])", tt.from_address_hash, tt.to_address_hash, tt.token_contract_address_hash)
})
|> wrapped_union_subquery()
end
query =
transactions_query
|> wrapped_union_subquery()
|> union(^internal_transactions_query)
|> union(^token_transfers_query)
from(
q in subquery(query),
select: fragment("COUNT(DISTINCT ?)", q.address_hash)
)
end
end

@ -13,6 +13,48 @@ defmodule Explorer.Prometheus.Instrumenter do
help: "Block import stage, runner and step in runner processing time"
]
@gauge [
name: :weekly_success_transactions_number,
help: "Number of successful transactions in the last 7 days",
registry: :public
]
@gauge [
name: :weekly_deployed_smart_contracts_number,
help: "Number of deployed smart-contracts in the last 7 days",
registry: :public
]
@gauge [
name: :weekly_verified_smart_contracts_number,
help: "Number of verified smart-contracts in the last 7 days",
registry: :public
]
@gauge [
name: :weekly_new_addresses_number,
help: "Number of new wallet addresses in the last 7 days",
registry: :public
]
@gauge [
name: :weekly_new_tokens_number,
help: "Number of new tokens detected in the last 7 days",
registry: :public
]
@gauge [
name: :weekly_new_token_transfers_number,
help: "Number of new token transfers detected in the last 7 days",
registry: :public
]
@gauge [
name: :weekly_active_addresses_number,
help: "Number of active EOA addresses (participated in transactions in to/from) in the last 7 days",
registry: :public
]
def block_import_stage_runner(function, stage, runner, step) do
{time, result} = :timer.tc(function)
@ -20,4 +62,32 @@ defmodule Explorer.Prometheus.Instrumenter do
result
end
def weekly_success_transactions_number(number) do
Gauge.set([name: :weekly_success_transactions_number, registry: :public], number)
end
def weekly_deployed_smart_contracts_number(number) do
Gauge.set([name: :weekly_deployed_smart_contracts_number, registry: :public], number)
end
def weekly_verified_smart_contracts_number(number) do
Gauge.set([name: :weekly_verified_smart_contracts_number, registry: :public], number)
end
def weekly_new_addresses_number(number) do
Gauge.set([name: :weekly_new_addresses_number, registry: :public], number)
end
def weekly_new_tokens_number(number) do
Gauge.set([name: :weekly_new_tokens_number, registry: :public], number)
end
def weekly_new_token_transfers_number(number) do
Gauge.set([name: :weekly_new_token_transfers_number, registry: :public], number)
end
def weekly_active_addresses_number(number) do
Gauge.set([name: :weekly_active_addresses_number, registry: :public], number)
end
end

@ -109,7 +109,7 @@ defmodule Explorer.Mixfile do
# `:spandex` tracing of `:ecto`
{:spandex_ecto, "~> 0.7.0"},
# Attach `:prometheus_ecto` to `:ecto`
{:telemetry, "~> 0.4.3"},
{:telemetry, "~> 1.2.1"},
# `Timex.Duration` for `Explorer.Counters.AverageBlockTime.average_block_time/0`
{:timex, "~> 3.7.1"},
{:con_cache, "~> 1.0"},

@ -25,7 +25,7 @@
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"},
"csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"},
@ -131,7 +131,7 @@
"spandex_phoenix": {:hex, :spandex_phoenix, "1.1.0", "9cff829d05258dd49a227c56711b19b69a8fd5d4873d8e9a92a4f4097e7322ab", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}, {:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "265fe05c1736485fbb75d66ef7576682ebf6428c391dd54d22217f612fd4ddad"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [: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.6", [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]}, {: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", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"},

Loading…
Cancel
Save