analytics/lib/plausible/geo.ex

208 lines
5.2 KiB
Elixir

defmodule Plausible.Geo do
@moduledoc """
This module provides an API for fetching IP geolocation.
"""
require Logger
@db :geolocation
@doc """
Starts the geodatabase loading process. Two modes are supported: local file
and MaxMind license key.
## Options
* `:path` - the path to the .mmdb database local file. When present,
`:license_key` and `:edition` are not required.
* `:license_key` - the [license key](https://support.maxmind.com/hc/en-us/articles/4407111582235-Generate-a-License-Key)
from MaxMind to authenticate requests to MaxMind.
* `:edition` - the name of the MaxMind database to be downloaded from MaxMind
servers. Defaults to `GeoLite2-City`.
* `:cache_dir` - if set, the downloaded .mmdb files are cached there across
restarts.
* `:async` - when used, configures the database loading to run
asynchronously.
## Examples
Loading from a local file:
iex> load_db(path: "/etc/plausible/dbip-city.mmdb")
:ok
Downloading a MaxMind DB (this license key is no longer active):
iex> load_db(license_key: "LNpsJCCKPis6XvBP", edition: "GeoLite2-City", async: true)
:ok
"""
def load_db(opts) do
cond do
license_key = opts[:license_key] ->
edition = opts[:edition] || "GeoLite2-City"
maxmind_opts = [license_key: license_key]
loader_opts =
if is_binary(opts[:cache_dir]) do
[
database_cache_file:
String.to_charlist(Path.join(opts[:cache_dir], edition <> ".mmdb.gz"))
]
else
[:no_cache]
end
:ok = :locus.start_loader(@db, {:maxmind, edition}, maxmind_opts ++ loader_opts)
path = opts[:path] ->
:ok = :locus.start_loader(@db, path)
true ->
raise "failed to load geolocation db: need :path or :license_key to be provided"
end
unless opts[:async] do
{:ok, _version} = :locus.await_loader(@db)
end
:ok
end
@doc """
Waits for the database to start after calling `load_db/1` with the async option.
"""
def await_loader, do: :locus.await_loader(@db)
@doc """
Returns geodatabase type.
Used for deciding whether to show the DB-IP disclaimer or not.
## Examples
In the case of a DB-IP database:
iex> database_type()
"DBIP-City-Lite"
In the case of a MaxMind database:
iex> database_type()
"GeoLite2-City"
"""
def database_type do
case :locus.get_info(@db, :metadata) do
{:ok, %{database_type: type}} -> type
_other -> nil
end
end
@doc """
Looks up geo info about an IP address.
## Examples
iex> lookup("8.7.6.5")
%{
"city" => %{
"geoname_id" => 5349755,
"names" => %{
"de" => "Fontana",
"en" => "Fontana",
"ja" => "フォンタナ",
"ru" => "Фонтана"
}
},
"continent" => %{
"code" => "NA",
"geoname_id" => 6255149,
"names" => %{
"de" => "Nordamerika",
"en" => "North America",
"es" => "Norteamérica",
"fr" => "Amérique du Nord",
"ja" => "北アメリカ",
"pt-BR" => "América do Norte",
"ru" => "Северная Америка",
"zh-CN" => "北美洲"
}
},
"country" => %{
"geoname_id" => 6252001,
"iso_code" => "US",
"names" => %{
"de" => "Vereinigte Staaten",
"en" => "United States",
"es" => "Estados Unidos",
"fr" => "États Unis",
"ja" => "アメリカ",
"pt-BR" => "EUA",
"ru" => "США",
"zh-CN" => "美国"
}
},
"location" => %{
"accuracy_radius" => 50,
"latitude" => 34.1211,
"longitude" => -117.4362,
"metro_code" => 803,
"time_zone" => "America/Los_Angeles"
},
"postal" => %{"code" => "92336"},
"registered_country" => %{
"geoname_id" => 6252001,
"iso_code" => "US",
"names" => %{
"de" => "Vereinigte Staaten",
"en" => "United States",
"es" => "Estados Unidos",
"fr" => "États Unis",
"ja" => "アメリカ",
"pt-BR" => "EUA",
"ru" => "США",
"zh-CN" => "美国"
}
},
"subdivisions" => [
%{
"geoname_id" => 5332921,
"iso_code" => "CA",
"names" => %{
"de" => "Kalifornien",
"en" => "California",
"es" => "California",
"fr" => "Californie",
"ja" => "カリフォルニア州",
"pt-BR" => "Califórnia",
"ru" => "Калифорния",
"zh-CN" => "加州"
}
}
]
}
"""
def lookup(ip_address) do
case :locus.lookup(@db, ip_address) do
{:ok, entry} ->
entry
:not_found ->
nil
{:error, {:invalid_address, _address}} ->
nil
{:error, reason} ->
Logger.error("Failed to lookup IP address. Reason: " <> inspect(reason))
nil
end
end
end