Skip to main content

relay_event_normalization/
geo.rs

1use std::fmt;
2use std::net::IpAddr;
3use std::path::Path;
4use std::sync::Arc;
5
6use relay_common::time::UnixTimestamp;
7use relay_event_schema::protocol::Geo;
8use relay_protocol::Annotated;
9
10#[cfg(feature = "mmap")]
11type ReaderType = maxminddb::Mmap;
12
13#[cfg(not(feature = "mmap"))]
14type ReaderType = Vec<u8>;
15
16/// An error in the `GeoIpLookup`.
17pub type GeoIpError = maxminddb::MaxMindDbError;
18
19/// A geo ip lookup helper based on maxmind db files.
20///
21/// The helper is internally reference counted and can be cloned cheaply.
22#[derive(Clone, Default)]
23pub struct GeoIpLookup(Option<Arc<maxminddb::Reader<ReaderType>>>);
24
25impl GeoIpLookup {
26    /// Opens a maxminddb file by path.
27    pub fn open<P>(path: P) -> Result<Self, GeoIpError>
28    where
29        P: AsRef<Path>,
30    {
31        #[cfg(feature = "mmap")]
32        let reader = unsafe { maxminddb::Reader::open_mmap(path)? };
33        #[cfg(not(feature = "mmap"))]
34        let reader = maxminddb::Reader::open_readfile(path)?;
35        Ok(GeoIpLookup(Some(Arc::new(reader))))
36    }
37
38    /// Creates a new [`GeoIpLookup`] instance without any data loaded.
39    pub fn empty() -> Self {
40        Self(None)
41    }
42
43    /// Unix timestamp when the database was built.
44    ///
45    /// Returns `None` for an [`Self::empty`] database.
46    pub fn build_epoch(&self) -> Option<UnixTimestamp> {
47        let reader = self.0.as_ref()?;
48        Some(UnixTimestamp::from_secs(reader.metadata.build_epoch))
49    }
50
51    /// Looks up an IP address.
52    pub fn try_lookup(&self, ip_address: IpAddr) -> Result<Option<Geo>, GeoIpError> {
53        let Some(reader) = self.0.as_ref() else {
54            return Ok(None);
55        };
56
57        let Some(city) = reader
58            .lookup(ip_address)?
59            .decode::<maxminddb::geoip2::City>()?
60        else {
61            return Ok(None);
62        };
63
64        Ok(Some(Geo {
65            country_code: Annotated::from(city.country.iso_code.map(String::from)),
66            city: Annotated::from(city.city.names.english.map(String::from)),
67            subdivision: Annotated::from(
68                city.subdivisions
69                    .first()
70                    .and_then(|subdivision| subdivision.names.english.map(String::from)),
71            ),
72            region: Annotated::from(city.country.names.english.map(String::from)),
73            ..Default::default()
74        }))
75    }
76
77    /// Like [`Self::try_lookup`], but swallows errors.
78    pub fn lookup(&self, ip_address: IpAddr) -> Option<Geo> {
79        self.try_lookup(ip_address).ok().flatten()
80    }
81}
82
83impl fmt::Debug for GeoIpLookup {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.debug_struct("GeoIpLookup").finish()
86    }
87}