401 lines
12 KiB
Elixir
401 lines
12 KiB
Elixir
defmodule Plausible.Application do
|
|
@moduledoc false
|
|
|
|
use Application
|
|
use Plausible
|
|
|
|
require Logger
|
|
|
|
def start(_type, _args) do
|
|
on_ee(do: Plausible.License.ensure_valid_license())
|
|
on_ce(do: :inet_db.set_tcp_module(:happy_tcp))
|
|
|
|
# in CE we start the endpoint under site_encrypt for automatic https
|
|
endpoint = on_ee(do: PlausibleWeb.Endpoint, else: maybe_https_endpoint())
|
|
|
|
cluster =
|
|
on_ee(
|
|
do:
|
|
{Cluster.Supervisor,
|
|
[
|
|
[
|
|
default: [
|
|
strategy: Cluster.Strategy.ErlangHosts,
|
|
config: [timeout: 30_000]
|
|
]
|
|
],
|
|
[name: Plausible.ClusterSupervisor]
|
|
]}
|
|
)
|
|
|
|
children =
|
|
[
|
|
cluster,
|
|
{PartitionSupervisor,
|
|
child_spec: Task.Supervisor, name: Plausible.UserAgentParseTaskSupervisor},
|
|
Plausible.Session.BalancerSupervisor,
|
|
Plausible.PromEx,
|
|
{Plausible.Auth.TOTP.Vault, key: totp_vault_key()},
|
|
Plausible.Repo,
|
|
Plausible.ClickhouseRepo,
|
|
Plausible.IngestRepo,
|
|
Plausible.AsyncInsertRepo,
|
|
Plausible.ImportDeletionRepo,
|
|
Plausible.Cache.Adapter.child_spec(:customer_currency, :cache_customer_currency,
|
|
ttl_check_interval: :timer.minutes(5),
|
|
n_lock_partitions: 1,
|
|
global_ttl: :timer.minutes(60)
|
|
),
|
|
Plausible.Cache.Adapter.child_spec(:user_agents, :cache_user_agents,
|
|
ttl_check_interval: :timer.minutes(5),
|
|
global_ttl: :timer.minutes(60),
|
|
n_lock_partitions: 1,
|
|
ets_options: [read_concurrency: true, write_concurrency: true]
|
|
),
|
|
Plausible.Cache.Adapter.child_specs(:sessions, :cache_sessions,
|
|
ttl_check_interval: :timer.seconds(10),
|
|
global_ttl: :timer.minutes(30),
|
|
n_lock_partitions: 1,
|
|
ets_options: [read_concurrency: true, write_concurrency: true]
|
|
),
|
|
{Plausible.Session.Transfer,
|
|
base_path: Application.get_env(:plausible, :session_transfer_dir)},
|
|
warmed_cache(Plausible.Site.Cache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.Site.Cache.All,
|
|
interval: :timer.minutes(15) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{Plausible.Site.Cache.RecentlyUpdated, interval: :timer.seconds(30)}
|
|
]
|
|
),
|
|
on_ee do
|
|
warmed_cache(Plausible.ConsolidatedView.Cache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.ConsolidatedView.Cache.All,
|
|
interval: :timer.minutes(20) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{Plausible.ConsolidatedView.Cache.RecentlyUpdated, interval: :timer.minutes(1)}
|
|
]
|
|
)
|
|
end,
|
|
warmed_cache(Plausible.Shield.IPRuleCache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.Shield.IPRuleCache.All,
|
|
interval: :timer.minutes(3) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{Plausible.Shield.IPRuleCache.RecentlyUpdated, interval: :timer.seconds(35)}
|
|
]
|
|
),
|
|
warmed_cache(Plausible.Shield.CountryRuleCache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.Shield.CountryRuleCache.All,
|
|
interval: :timer.minutes(3) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{Plausible.Shield.CountryRuleCache.RecentlyUpdated, interval: :timer.seconds(35)}
|
|
]
|
|
),
|
|
warmed_cache(Plausible.Shield.PageRuleCache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [:bag, read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.Shield.PageRuleCache.All,
|
|
interval: :timer.minutes(3) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{Plausible.Shield.PageRuleCache.RecentlyUpdated, interval: :timer.seconds(35)}
|
|
]
|
|
),
|
|
warmed_cache(Plausible.Shield.HostnameRuleCache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [:bag, read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.Shield.HostnameRuleCache.All,
|
|
interval: :timer.minutes(3) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{Plausible.Shield.HostnameRuleCache.RecentlyUpdated, interval: :timer.seconds(25)}
|
|
]
|
|
),
|
|
on_ee do
|
|
warmed_cache(Plausible.Stats.SamplingCache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
read_concurrency: true
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{Plausible.Stats.SamplingCache.All,
|
|
interval: :timer.hours(24) + Enum.random(1..:timer.minutes(60))}
|
|
]
|
|
)
|
|
end,
|
|
warmed_cache(PlausibleWeb.TrackerScriptCache,
|
|
adapter_opts: [
|
|
n_lock_partitions: 1,
|
|
ttl_check_interval: false,
|
|
ets_options: [:bag, read_concurrency: true]
|
|
],
|
|
warmers: [
|
|
refresh_all:
|
|
{PlausibleWeb.TrackerScriptCache.All,
|
|
interval: :timer.minutes(180) + Enum.random(1..:timer.seconds(10))},
|
|
refresh_updated_recently:
|
|
{PlausibleWeb.TrackerScriptCache.RecentlyUpdated, interval: :timer.seconds(30)}
|
|
]
|
|
),
|
|
Plausible.Ingestion.Counters,
|
|
Plausible.Session.Salts,
|
|
Supervisor.child_spec(Plausible.Event.WriteBuffer, id: Plausible.Event.WriteBuffer),
|
|
Supervisor.child_spec(Plausible.Session.WriteBuffer, id: Plausible.Session.WriteBuffer),
|
|
ReferrerBlocklist,
|
|
{Plausible.RateLimit, clean_period: :timer.minutes(10)},
|
|
{Finch, name: Plausible.Finch, pools: finch_pool_config()},
|
|
{Phoenix.PubSub, name: Plausible.PubSub},
|
|
endpoint,
|
|
{Oban, Application.get_env(:plausible, Oban)},
|
|
on_ee do
|
|
help_scout_vault()
|
|
end
|
|
]
|
|
|> List.flatten()
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
opts = [strategy: :one_for_one, name: Plausible.Supervisor]
|
|
|
|
setup_request_logging()
|
|
setup_sentry()
|
|
setup_opentelemetry()
|
|
Plausible.Ingestion.Persistor.TelemetryHandler.install()
|
|
|
|
setup_geolocation()
|
|
Location.load_all()
|
|
Plausible.Geo.await_loader()
|
|
|
|
Supervisor.start_link(List.flatten(children), opts)
|
|
end
|
|
|
|
def config_change(changed, _new, removed) do
|
|
PlausibleWeb.Endpoint.config_change(changed, removed)
|
|
:ok
|
|
end
|
|
|
|
on_ee do
|
|
defp help_scout_vault() do
|
|
help_scout_vault_key =
|
|
:plausible
|
|
|> Application.fetch_env!(Plausible.HelpScout)
|
|
|> Keyword.fetch!(:vault_key)
|
|
|> Base.decode64!()
|
|
|
|
[{Plausible.HelpScout.Vault, key: help_scout_vault_key}]
|
|
end
|
|
end
|
|
|
|
defp totp_vault_key() do
|
|
:plausible
|
|
|> Application.fetch_env!(Plausible.Auth.TOTP)
|
|
|> Keyword.fetch!(:vault_key)
|
|
end
|
|
|
|
defp finch_pool_config() do
|
|
default = Application.get_env(:plausible, Plausible.Finch)
|
|
|
|
base_config =
|
|
if default do
|
|
%{default: default}
|
|
else
|
|
%{}
|
|
end
|
|
|
|
default_opts = default || []
|
|
|
|
base_config
|
|
|> Map.put(
|
|
"https://icons.duckduckgo.com",
|
|
Config.Reader.merge(default_opts, conn_opts: [transport_opts: [timeout: 15_000]])
|
|
)
|
|
|> maybe_add_persistor_pool(default_opts)
|
|
|> maybe_add_sentry_pool(default_opts)
|
|
|> maybe_add_paddle_pool(default_opts)
|
|
|> maybe_add_google_pools(default_opts)
|
|
end
|
|
|
|
defp maybe_add_sentry_pool(pool_config, default) do
|
|
case Sentry.Config.dsn() do
|
|
%{endpoint_uri: "http" <> _rest = url} ->
|
|
Map.put(pool_config, url, Config.Reader.merge(default, size: 50))
|
|
|
|
nil ->
|
|
pool_config
|
|
end
|
|
end
|
|
|
|
defp maybe_add_persistor_pool(pool_config, default) do
|
|
backend =
|
|
:plausible
|
|
|> Application.fetch_env!(Plausible.Ingestion.Persistor)
|
|
|> Keyword.fetch!(:backend)
|
|
|
|
persistor_conf = Application.get_env(:plausible, Plausible.Ingestion.Persistor.Remote)
|
|
|
|
if backend in [
|
|
Plausible.Ingestion.Persistor.Remote,
|
|
Plausible.Ingestion.Persistor.EmbeddedWithRelay
|
|
] do
|
|
persistor_url = Keyword.fetch!(persistor_conf, :url)
|
|
count = Keyword.fetch!(persistor_conf, :count)
|
|
timeout_ms = Keyword.fetch!(persistor_conf, :timeout_ms)
|
|
|
|
Map.put(
|
|
pool_config,
|
|
persistor_url,
|
|
Config.Reader.merge(
|
|
default,
|
|
protocols: [:http2],
|
|
count: count,
|
|
conn_opts: [transport_opts: [timeout: timeout_ms, nodelay: false]]
|
|
)
|
|
)
|
|
else
|
|
pool_config
|
|
end
|
|
end
|
|
|
|
defp maybe_add_paddle_pool(pool_config, default) do
|
|
paddle_conf = Application.get_env(:plausible, :paddle)
|
|
|
|
cond do
|
|
paddle_conf[:vendor_id] && paddle_conf[:vendor_auth_code] ->
|
|
Map.put(
|
|
pool_config,
|
|
Plausible.Billing.PaddleApi.vendors_domain(),
|
|
Config.Reader.merge(default, conn_opts: [transport_opts: [timeout: 15_000]])
|
|
)
|
|
|
|
true ->
|
|
pool_config
|
|
end
|
|
end
|
|
|
|
defp maybe_add_google_pools(pool_config, default) do
|
|
google_conf = Application.get_env(:plausible, :google)
|
|
|
|
cond do
|
|
google_conf[:client_id] && google_conf[:client_secret] ->
|
|
pool_config
|
|
|> Map.put(
|
|
google_conf[:api_url],
|
|
Config.Reader.merge(default, conn_opts: [transport_opts: [timeout: 15_000]])
|
|
)
|
|
|> Map.put(
|
|
google_conf[:reporting_api_url],
|
|
Config.Reader.merge(default, conn_opts: [transport_opts: [timeout: 15_000]])
|
|
)
|
|
|
|
true ->
|
|
pool_config
|
|
end
|
|
end
|
|
|
|
def setup_request_logging() do
|
|
:telemetry.attach(
|
|
"plausible-request-logging",
|
|
[:phoenix, :endpoint, :stop],
|
|
&Plausible.RequestLogger.log_request/4,
|
|
%{}
|
|
)
|
|
end
|
|
|
|
def setup_sentry() do
|
|
Logger.add_backend(Sentry.LoggerBackend)
|
|
|
|
:telemetry.attach_many(
|
|
"oban-errors",
|
|
[[:oban, :job, :exception], [:oban, :notifier, :exception], [:oban, :plugin, :exception]],
|
|
&ObanErrorReporter.handle_event/4,
|
|
%{}
|
|
)
|
|
end
|
|
|
|
defp setup_opentelemetry() do
|
|
OpentelemetryPhoenix.setup()
|
|
OpentelemetryEcto.setup([:plausible, :repo])
|
|
OpentelemetryEcto.setup([:plausible, :clickhouse_repo])
|
|
OpentelemetryOban.setup()
|
|
end
|
|
|
|
defp setup_geolocation do
|
|
opts = Application.fetch_env!(:plausible, Plausible.Geo)
|
|
:ok = Plausible.Geo.load_db(opts)
|
|
end
|
|
|
|
defp warmed_cache(impl_mod, opts) when is_atom(impl_mod) and is_list(opts) do
|
|
warmers = Keyword.fetch!(opts, :warmers)
|
|
|
|
warmer_specs =
|
|
Enum.map(warmers, fn {warmer_fn, {warmer_id, warmer_opts}} ->
|
|
{Plausible.Cache.Warmer,
|
|
Keyword.merge(
|
|
[
|
|
child_name: warmer_id,
|
|
cache_impl: impl_mod,
|
|
warmer_fn: warmer_fn
|
|
],
|
|
warmer_opts
|
|
)}
|
|
end)
|
|
|
|
[{impl_mod, Keyword.fetch!(opts, :adapter_opts)} | warmer_specs]
|
|
end
|
|
|
|
on_ce do
|
|
defp maybe_https_endpoint do
|
|
endpoint_config = Application.fetch_env!(:plausible, PlausibleWeb.Endpoint)
|
|
selfhost_config = Application.fetch_env!(:plausible, :selfhost)
|
|
site_encrypt_config = Keyword.get(selfhost_config, :site_encrypt)
|
|
|
|
if get_in(endpoint_config, [:https, :port]) do
|
|
PlausibleWeb.Endpoint.force_https()
|
|
end
|
|
|
|
if site_encrypt_config do
|
|
PlausibleWeb.Endpoint.allow_acme_challenges()
|
|
{SiteEncrypt.Phoenix.Endpoint, endpoint: PlausibleWeb.Endpoint}
|
|
else
|
|
PlausibleWeb.Endpoint
|
|
end
|
|
end
|
|
end
|
|
end
|