vector/enrichment_tables/
mmdb.rs1use std::{fs, net::IpAddr, path::PathBuf, sync::Arc, time::SystemTime};
6
7use maxminddb::Reader;
8use vector_lib::{
9 configurable::configurable_component,
10 enrichment::{Case, Condition, Error, IndexHandle, Table},
11};
12use vrl::value::{ObjectMap, Value};
13
14use crate::config::{EnrichmentTableConfig, GenerateConfig};
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18#[configurable_component(enrichment_table("mmdb"))]
19pub struct MmdbConfig {
20 pub path: PathBuf,
24}
25
26impl GenerateConfig for MmdbConfig {
27 fn generate_config() -> toml::Value {
28 toml::Value::try_from(Self {
29 path: "/path/to/GeoLite2-City.mmdb".into(),
30 })
31 .unwrap()
32 }
33}
34
35impl EnrichmentTableConfig for MmdbConfig {
36 async fn build(
37 &self,
38 _: &crate::config::GlobalOptions,
39 _: Option<Box<dyn std::any::Any + Send + Sync>>,
40 ) -> crate::Result<Box<dyn Table + Send + Sync>> {
41 Ok(Box::new(Mmdb::new(self.clone())?))
42 }
43}
44
45#[derive(Clone)]
46pub struct Mmdb {
48 config: MmdbConfig,
49 dbreader: Arc<maxminddb::Reader<Vec<u8>>>,
50 last_modified: SystemTime,
51}
52
53impl Mmdb {
54 pub fn new(config: MmdbConfig) -> crate::Result<Self> {
56 let dbreader = Arc::new(Reader::open_readfile(&config.path)?);
57
58 let ip = IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
60 let result = dbreader.lookup(ip)?.decode::<ObjectMap>().map(|_| ());
61
62 match result {
63 Ok(_) => Ok(Mmdb {
64 last_modified: fs::metadata(&config.path)?.modified()?,
65 dbreader,
66 config,
67 }),
68 Err(error) => Err(error.into()),
69 }
70 }
71
72 fn lookup(&self, ip: IpAddr, select: Option<&[String]>) -> Option<ObjectMap> {
73 let data = self.dbreader.lookup(ip).ok()?.decode().ok()??;
74
75 if let Some(fields) = select {
76 let mut filtered = Value::from(ObjectMap::new());
77 let mut data_value = Value::from(data);
78 for field in fields {
79 let Ok(field_path) = vrl::path::parse_value_path(field) else {
84 continue;
85 };
86 filtered.insert(
87 &field_path,
88 data_value.remove(&field_path, false).unwrap_or(Value::Null),
89 );
90 }
91 filtered.into_object()
92 } else {
93 Some(data)
94 }
95 }
96}
97
98impl Table for Mmdb {
99 fn find_table_row<'a>(
105 &self,
106 case: Case,
107 condition: &'a [Condition<'a>],
108 select: Option<&[String]>,
109 wildcard: Option<&Value>,
110 index: Option<IndexHandle>,
111 ) -> Result<ObjectMap, Error> {
112 let mut rows = self.find_table_rows(case, condition, select, wildcard, index)?;
113
114 match rows.pop() {
115 Some(row) if rows.is_empty() => Ok(row),
116 Some(_) => Err(Error::MoreThanOneRowFound),
117 None => Err(Error::NoRowsFound),
118 }
119 }
120
121 fn find_table_rows<'a>(
125 &self,
126 _: Case,
127 condition: &'a [Condition<'a>],
128 select: Option<&[String]>,
129 _wildcard: Option<&Value>,
130 _: Option<IndexHandle>,
131 ) -> Result<Vec<ObjectMap>, Error> {
132 match condition.first() {
133 Some(_) if condition.len() > 1 => Err(Error::OnlyOneConditionAllowed),
134 Some(Condition::Equals { value, .. }) => {
135 let ip = value
136 .to_string_lossy()
137 .parse::<IpAddr>()
138 .map_err(|source| Error::InvalidAddress { source })?;
139 Ok(self
140 .lookup(ip, select)
141 .map(|values| vec![values])
142 .unwrap_or_default())
143 }
144 Some(_) => Err(Error::OnlyEqualityConditionAllowed),
145 None => Err(Error::MissingCondition { kind: "IP" }),
146 }
147 }
148
149 fn add_index(&mut self, _: Case, fields: &[&str]) -> Result<IndexHandle, Error> {
155 match fields.len() {
156 0 => Err(Error::MissingRequiredField { field: "IP" }),
157 1 => Ok(IndexHandle(0)),
158 _ => Err(Error::OnlyOneFieldAllowed),
159 }
160 }
161
162 fn index_fields(&self) -> Vec<(Case, Vec<String>)> {
164 Vec::new()
165 }
166
167 fn needs_reload(&self) -> bool {
169 matches!(fs::metadata(&self.config.path)
170 .and_then(|metadata| metadata.modified()),
171 Ok(modified) if modified > self.last_modified)
172 }
173}
174
175impl std::fmt::Debug for Mmdb {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 write!(f, "Maxmind database {})", self.config.path.display())
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use vrl::value::Value;
184
185 use super::*;
186
187 #[test]
188 fn city_partial_lookup() {
189 let values = find_select(
190 "2.125.160.216",
191 "tests/data/GeoIP2-City-Test.mmdb",
192 Some(&[
193 "location.latitude".to_string(),
194 "location.longitude".to_string(),
195 ]),
196 )
197 .unwrap();
198
199 let mut expected = ObjectMap::new();
200 expected.insert(
201 "location".into(),
202 ObjectMap::from([
203 ("latitude".into(), Value::from(51.75)),
204 ("longitude".into(), Value::from(-1.25)),
205 ])
206 .into(),
207 );
208
209 assert_eq!(values, expected);
210 }
211
212 #[test]
213 fn city_partial_lookup_skips_unparseable_selects() {
214 let values = find_select(
217 "2.125.160.216",
218 "tests/data/GeoIP2-City-Test.mmdb",
219 Some(&[
220 "location.latitude".to_string(),
221 "not a valid path".to_string(),
222 ]),
223 )
224 .unwrap();
225
226 let mut expected = ObjectMap::new();
227 expected.insert(
228 "location".into(),
229 ObjectMap::from([("latitude".into(), Value::from(51.75))]).into(),
230 );
231
232 assert_eq!(values, expected);
233 }
234
235 #[test]
236 fn isp_lookup() {
237 let values = find("208.192.1.2", "tests/data/GeoIP2-ISP-Test.mmdb").unwrap();
238
239 let mut expected = ObjectMap::new();
240 expected.insert("autonomous_system_number".into(), 701i64.into());
241 expected.insert(
242 "autonomous_system_organization".into(),
243 "MCI Communications Services, Inc. d/b/a Verizon Business".into(),
244 );
245 expected.insert("isp".into(), "Verizon Business".into());
246 expected.insert("organization".into(), "Verizon Business".into());
247
248 assert_eq!(values, expected);
249 }
250
251 #[test]
252 fn connection_type_lookup_success() {
253 let values = find(
254 "201.243.200.1",
255 "tests/data/GeoIP2-Connection-Type-Test.mmdb",
256 )
257 .unwrap();
258
259 let mut expected = ObjectMap::new();
260 expected.insert("connection_type".into(), "Corporate".into());
261
262 assert_eq!(values, expected);
263 }
264
265 #[test]
266 fn lookup_missing() {
267 let values = find("10.1.12.1", "tests/data/custom-type.mmdb");
268
269 assert!(values.is_none());
270 }
271
272 #[test]
273 fn custom_mmdb_type() {
274 let values = find("208.192.1.2", "tests/data/custom-type.mmdb").unwrap();
275
276 let mut expected = ObjectMap::new();
277 expected.insert("hostname".into(), "custom".into());
278 expected.insert(
279 "nested".into(),
280 ObjectMap::from([
281 ("hostname".into(), "custom".into()),
282 ("original_cidr".into(), "208.192.1.2/24".into()),
283 ])
284 .into(),
285 );
286
287 assert_eq!(values, expected);
288 }
289
290 fn find(ip: &str, database: &str) -> Option<ObjectMap> {
291 find_select(ip, database, None)
292 }
293
294 fn find_select(ip: &str, database: &str, select: Option<&[String]>) -> Option<ObjectMap> {
295 Mmdb::new(MmdbConfig {
296 path: database.into(),
297 })
298 .unwrap()
299 .find_table_rows(
300 Case::Insensitive,
301 &[Condition::Equals {
302 field: "ip",
303 value: ip.into(),
304 }],
305 select,
306 None,
307 None,
308 )
309 .unwrap()
310 .pop()
311 }
312}