nginx_discovery/discovery.rs
1//! High-level discovery API for NGINX configurations
2//!
3//! This module provides a convenient API for discovering and analyzing NGINX configurations.
4//!
5//! # Examples
6//!
7//! ## Parse from text
8//!
9//! ```
10//! use nginx_discovery::NginxDiscovery;
11//!
12//! let config = r"
13//! http {
14//! access_log /var/log/nginx/access.log;
15//! }
16//! ";
17//!
18//! let discovery = NginxDiscovery::from_config_text(config)?;
19//! let logs = discovery.access_logs();
20//! assert_eq!(logs.len(), 1);
21//! # Ok::<(), nginx_discovery::Error>(())
22//! ```
23//!
24//! ## Parse from file
25//!
26//! ```no_run
27//! use nginx_discovery::NginxDiscovery;
28//!
29//! let discovery = NginxDiscovery::from_config_file("/etc/nginx/nginx.conf")?;
30//! let logs = discovery.access_logs();
31//! let formats = discovery.log_formats();
32//! # Ok::<(), nginx_discovery::Error>(())
33//! ```
34
35use crate::ast::Config;
36use crate::error::Result;
37use crate::extract;
38use crate::prelude::Server;
39use crate::types::{AccessLog, LogFormat};
40use std::path::{Path, PathBuf};
41
42/// High-level NGINX configuration discovery
43///
44/// Provides convenient methods to discover and analyze NGINX configurations.
45#[derive(Debug, Clone)]
46pub struct NginxDiscovery {
47 /// Parsed configuration
48 config: Config,
49 /// Path to the configuration file (if loaded from file)
50 config_path: Option<PathBuf>,
51}
52
53impl NginxDiscovery {
54 /// Create a discovery instance from configuration text
55 ///
56 /// # Arguments
57 ///
58 /// * `text` - NGINX configuration as a string
59 ///
60 /// # Errors
61 ///
62 /// Returns an error if the configuration cannot be parsed.
63 ///
64 /// # Examples
65 ///
66 /// ```
67 /// use nginx_discovery::NginxDiscovery;
68 ///
69 /// let config = "user nginx;";
70 /// let discovery = NginxDiscovery::from_config_text(config)?;
71 /// # Ok::<(), nginx_discovery::Error>(())
72 /// ```
73 pub fn from_config_text(text: &str) -> Result<Self> {
74 let config = crate::parse(text)?;
75 Ok(Self {
76 config,
77 config_path: None,
78 })
79 }
80
81 /// Create a discovery instance from a configuration file
82 ///
83 /// # Arguments
84 ///
85 /// * `path` - Path to the NGINX configuration file
86 ///
87 /// # Errors
88 ///
89 /// Returns an error if:
90 /// - The file cannot be read
91 /// - The configuration cannot be parsed
92 ///
93 /// # Examples
94 ///
95 /// ```no_run
96 /// use nginx_discovery::NginxDiscovery;
97 ///
98 /// let discovery = NginxDiscovery::from_config_file("/etc/nginx/nginx.conf")?;
99 /// # Ok::<(), nginx_discovery::Error>(())
100 /// ```
101 pub fn from_config_file(path: impl AsRef<Path>) -> Result<Self> {
102 let path = path.as_ref();
103 let text = std::fs::read_to_string(path)?;
104 let config = crate::parse(&text)?;
105 Ok(Self {
106 config,
107 config_path: Some(path.to_path_buf()),
108 })
109 }
110
111 /// Create a discovery instance from a running NGINX instance
112 ///
113 /// This attempts to:
114 /// 1. Find the nginx binary
115 /// 2. Run `nginx -T` to dump the configuration
116 /// 3. Parse the dumped configuration
117 ///
118 /// # Errors
119 ///
120 /// Returns an error if:
121 /// - nginx binary cannot be found
122 /// - nginx -T fails to execute
123 /// - The configuration cannot be parsed
124 /// - Insufficient permissions to run nginx -T
125 ///
126 /// # Examples
127 ///
128 /// ```no_run
129 /// use nginx_discovery::NginxDiscovery;
130 ///
131 /// let discovery = NginxDiscovery::from_running_instance()?;
132 /// let logs = discovery.access_logs();
133 /// # Ok::<(), nginx_discovery::Error>(())
134 /// ```
135 #[cfg(feature = "system")]
136 pub fn from_running_instance() -> Result<Self> {
137 crate::system::detect_and_parse()
138 }
139
140 /// Get all access log configurations
141 ///
142 /// Returns all `access_log` directives found in the configuration,
143 /// including those in http, server, and location contexts.
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use nginx_discovery::NginxDiscovery;
149 ///
150 /// let config = r"
151 /// http {
152 /// access_log /var/log/nginx/access.log;
153 /// server {
154 /// access_log /var/log/nginx/server.log;
155 /// }
156 /// }
157 /// ";
158 ///
159 /// let discovery = NginxDiscovery::from_config_text(config)?;
160 /// let logs = discovery.access_logs();
161 /// assert_eq!(logs.len(), 2);
162 /// # Ok::<(), nginx_discovery::Error>(())
163 /// ```
164 #[must_use]
165 pub fn access_logs(&self) -> Vec<AccessLog> {
166 extract::access_logs(&self.config).unwrap_or_default()
167 }
168
169 /// Get all log format definitions
170 ///
171 /// Returns all `log_format` directives found in the configuration.
172 ///
173 /// # Examples
174 ///
175 /// ```
176 /// use nginx_discovery::NginxDiscovery;
177 ///
178 /// let config = r"
179 /// log_format combined '$remote_addr - $remote_user [$time_local]';
180 /// log_format main '$request $status';
181 /// ";
182 ///
183 /// let discovery = NginxDiscovery::from_config_text(config)?;
184 /// let formats = discovery.log_formats();
185 /// assert_eq!(formats.len(), 2);
186 /// # Ok::<(), nginx_discovery::Error>(())
187 /// ```
188 #[must_use]
189 pub fn log_formats(&self) -> Vec<LogFormat> {
190 extract::log_formats(&self.config).unwrap_or_default()
191 }
192
193 /// Get all log file paths (access logs only)
194 ///
195 /// Returns a deduplicated list of all access log file paths.
196 ///
197 /// # Examples
198 ///
199 /// ```
200 /// use nginx_discovery::NginxDiscovery;
201 ///
202 /// let config = r"
203 /// access_log /var/log/nginx/access.log;
204 /// access_log /var/log/nginx/access.log; # duplicate
205 /// access_log /var/log/nginx/other.log;
206 /// ";
207 ///
208 /// let discovery = NginxDiscovery::from_config_text(config)?;
209 /// let files = discovery.all_log_files();
210 /// assert_eq!(files.len(), 2); // deduplicated
211 /// # Ok::<(), nginx_discovery::Error>(())
212 /// ```
213 #[must_use]
214 pub fn all_log_files(&self) -> Vec<PathBuf> {
215 let mut paths: Vec<PathBuf> = self.access_logs().into_iter().map(|log| log.path).collect();
216
217 // Deduplicate
218 paths.sort();
219 paths.dedup();
220 paths
221 }
222
223 /// Get all server names from server blocks
224 ///
225 /// Returns a list of all server names defined in server blocks.
226 ///
227 /// # Examples
228 ///
229 /// ```
230 /// use nginx_discovery::NginxDiscovery;
231 ///
232 /// let config = r"
233 /// server {
234 /// server_name example.com www.example.com;
235 /// }
236 /// server {
237 /// server_name test.com;
238 /// }
239 /// ";
240 ///
241 /// let discovery = NginxDiscovery::from_config_text(config)?;
242 /// let names = discovery.server_names();
243 /// assert_eq!(names.len(), 3);
244 /// # Ok::<(), nginx_discovery::Error>(())
245 /// ```
246 #[must_use]
247 pub fn server_names(&self) -> Vec<String> {
248 let mut names = Vec::new();
249
250 for server in self.config.find_directives_recursive("server") {
251 for server_name_directive in server.find_children("server_name") {
252 names.extend(server_name_directive.args_as_strings());
253 }
254 }
255
256 names
257 }
258
259 /// Export configuration to JSON
260 ///
261 /// # Errors
262 ///
263 /// Returns an error if serialization fails.
264 ///
265 /// # Examples
266 ///
267 /// ```
268 /// use nginx_discovery::NginxDiscovery;
269 ///
270 /// let config = "user nginx;";
271 /// let discovery = NginxDiscovery::from_config_text(config)?;
272 /// let json = discovery.to_json()?;
273 /// assert!(json.contains("user"));
274 /// # Ok::<(), nginx_discovery::Error>(())
275 /// ```
276 #[cfg(feature = "serde")]
277 pub fn to_json(&self) -> Result<String> {
278 serde_json::to_string_pretty(&self.config)
279 .map_err(|e| crate::Error::Serialization(e.to_string()))
280 }
281
282 /// Export configuration to YAML
283 ///
284 /// # Errors
285 ///
286 /// Returns an error if serialization fails.
287 ///
288 /// # Examples
289 ///
290 /// ```
291 /// use nginx_discovery::NginxDiscovery;
292 ///
293 /// let config = "user nginx;";
294 /// let discovery = NginxDiscovery::from_config_text(config)?;
295 /// let yaml = discovery.to_yaml()?;
296 /// assert!(yaml.contains("user"));
297 /// # Ok::<(), nginx_discovery::Error>(())
298 /// ```
299 #[cfg(feature = "serde")]
300 pub fn to_yaml(&self) -> Result<String> {
301 serde_yaml::to_string(&self.config).map_err(|e| crate::Error::Serialization(e.to_string()))
302 }
303
304 /// Get the parsed configuration AST
305 ///
306 /// Provides direct access to the parsed configuration for custom processing.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use nginx_discovery::NginxDiscovery;
312 ///
313 /// let config = "user nginx;";
314 /// let discovery = NginxDiscovery::from_config_text(config)?;
315 /// let ast = discovery.config();
316 /// assert_eq!(ast.directives.len(), 1);
317 /// # Ok::<(), nginx_discovery::Error>(())
318 /// ```
319 #[must_use]
320 pub fn config(&self) -> &Config {
321 &self.config
322 }
323
324 /// Get the configuration file path (if loaded from file)
325 ///
326 /// # Examples
327 ///
328 /// ```no_run
329 /// use nginx_discovery::NginxDiscovery;
330 ///
331 /// let discovery = NginxDiscovery::from_config_file("/etc/nginx/nginx.conf")?;
332 /// assert!(discovery.config_path().is_some());
333 /// # Ok::<(), nginx_discovery::Error>(())
334 /// ```
335 #[must_use]
336 pub fn config_path(&self) -> Option<&Path> {
337 self.config_path.as_deref()
338 }
339
340 /// Generate a summary of the configuration
341 ///
342 /// Returns a human-readable summary including:
343 /// - Number of directives
344 /// - Number of server blocks
345 /// - Number of access logs
346 /// - Number of log formats
347 ///
348 /// # Examples
349 ///
350 /// ```
351 /// use nginx_discovery::NginxDiscovery;
352 ///
353 /// let config = r"
354 /// user nginx;
355 /// access_log /var/log/nginx/access.log;
356 /// server { listen 80; }
357 /// ";
358 ///
359 /// let discovery = NginxDiscovery::from_config_text(config)?;
360 /// let summary = discovery.summary();
361 /// assert!(summary.contains("directives"));
362 /// # Ok::<(), nginx_discovery::Error>(())
363 /// ```
364 #[must_use]
365 pub fn summary(&self) -> String {
366 let directive_count = self.config.count_directives();
367 let server_count = self.config.find_directives_recursive("server").len();
368 let access_log_count = self.access_logs().len();
369 let format_count = self.log_formats().len();
370
371 format!(
372 "NGINX Configuration Summary:\n\
373 - Total directives: {directive_count}\n\
374 - Server blocks: {server_count}\n\
375 - Access logs: {access_log_count}\n\
376 - Log formats: {format_count}"
377 )
378 }
379
380 // Add these methods to the NginxDiscovery impl block:
381
382 /// Get all server blocks
383 ///
384 /// Returns all `server` blocks found in the configuration.
385 ///
386 /// # Examples
387 ///
388 /// ```
389 /// use nginx_discovery::NginxDiscovery;
390 ///
391 /// let config = r"
392 /// server {
393 /// listen 80;
394 /// server_name example.com;
395 /// }
396 /// ";
397 ///
398 /// let discovery = NginxDiscovery::from_config_text(config)?;
399 /// let servers = discovery.servers();
400 /// assert_eq!(servers.len(), 1);
401 /// # Ok::<(), nginx_discovery::Error>(())
402 /// ```
403 #[must_use]
404 pub fn servers(&self) -> Vec<crate::types::Server> {
405 extract::servers(&self.config).unwrap_or_default()
406 }
407
408 /// Get all listening ports
409 ///
410 /// Returns a deduplicated list of all ports that servers are listening on.
411 ///
412 /// # Examples
413 ///
414 /// ```
415 /// use nginx_discovery::NginxDiscovery;
416 ///
417 /// let config = r"
418 /// server {
419 /// listen 80;
420 /// listen 443 ssl;
421 /// }
422 /// ";
423 ///
424 /// let discovery = NginxDiscovery::from_config_text(config)?;
425 /// let ports = discovery.listening_ports();
426 /// assert!(ports.contains(&80));
427 /// assert!(ports.contains(&443));
428 /// # Ok::<(), nginx_discovery::Error>(())
429 /// ```
430 #[must_use]
431 pub fn listening_ports(&self) -> Vec<u16> {
432 let mut ports: Vec<u16> = self
433 .servers()
434 .iter()
435 .flat_map(|s| s.listen.iter().map(|l| l.port))
436 .collect();
437
438 ports.sort_unstable();
439 ports.dedup();
440 ports
441 }
442
443 /// Get all SSL-enabled servers
444 ///
445 /// Returns servers that have SSL configured.
446 ///
447 /// # Examples
448 ///
449 /// ```
450 /// use nginx_discovery::NginxDiscovery;
451 ///
452 /// let config = r"
453 /// server {
454 /// listen 80;
455 /// server_name example.com;
456 /// }
457 /// server {
458 /// listen 443 ssl;
459 /// server_name secure.example.com;
460 /// }
461 /// ";
462 ///
463 /// let discovery = NginxDiscovery::from_config_text(config)?;
464 /// let ssl_servers = discovery.ssl_servers();
465 /// assert_eq!(ssl_servers.len(), 1);
466 /// # Ok::<(), nginx_discovery::Error>(())
467 /// ```
468 #[must_use]
469 pub fn ssl_servers(&self) -> Vec<crate::types::Server> {
470 self.servers().into_iter().filter(Server::has_ssl).collect()
471 }
472
473 /// Get all proxy locations
474 ///
475 /// Returns all location blocks that have `proxy_pass` configured.
476 ///
477 /// # Examples
478 ///
479 /// ```
480 /// use nginx_discovery::NginxDiscovery;
481 ///
482 /// let config = r"
483 /// server {
484 /// location / {
485 /// root /var/www;
486 /// }
487 /// location /api {
488 /// proxy_pass http://backend;
489 /// }
490 /// }
491 /// ";
492 ///
493 /// let discovery = NginxDiscovery::from_config_text(config)?;
494 /// let proxies = discovery.proxy_locations();
495 /// assert_eq!(proxies.len(), 1);
496 /// # Ok::<(), nginx_discovery::Error>(())
497 /// ```
498 #[must_use]
499 pub fn proxy_locations(&self) -> Vec<crate::types::Location> {
500 self.servers()
501 .iter()
502 .flat_map(|s| s.locations.iter())
503 .filter(|l: &&crate::types::Location| l.is_proxy())
504 .cloned()
505 .collect()
506 }
507
508 /// Count total number of location blocks
509 #[must_use]
510 pub fn location_count(&self) -> usize {
511 self.servers().iter().map(|s| s.locations.len()).sum()
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518
519 #[test]
520 fn test_from_config_text() {
521 let config = "user nginx;";
522 let discovery = NginxDiscovery::from_config_text(config).unwrap();
523 assert_eq!(discovery.config.directives.len(), 1);
524 }
525
526 #[test]
527 fn test_access_logs() {
528 let config = r"
529 http {
530 access_log /var/log/nginx/access.log;
531 server {
532 access_log /var/log/nginx/server.log;
533 }
534 }
535 ";
536
537 let discovery = NginxDiscovery::from_config_text(config).unwrap();
538 let logs = discovery.access_logs();
539 assert_eq!(logs.len(), 2);
540 }
541
542 #[test]
543 fn test_log_formats() {
544 let config = r"
545 log_format combined '$remote_addr';
546 log_format main '$request';
547 ";
548
549 let discovery = NginxDiscovery::from_config_text(config).unwrap();
550 let formats = discovery.log_formats();
551 assert_eq!(formats.len(), 2);
552 }
553
554 #[test]
555 fn test_all_log_files() {
556 let config = r"
557 access_log /var/log/nginx/access.log;
558 access_log /var/log/nginx/access.log;
559 access_log /var/log/nginx/other.log;
560 ";
561
562 let discovery = NginxDiscovery::from_config_text(config).unwrap();
563 let files = discovery.all_log_files();
564 assert_eq!(files.len(), 2); // Deduplicated
565 }
566
567 #[test]
568 fn test_server_names() {
569 let config = r"
570 server {
571 server_name example.com www.example.com;
572 }
573 server {
574 server_name test.com;
575 }
576 ";
577
578 let discovery = NginxDiscovery::from_config_text(config).unwrap();
579 let names = discovery.server_names();
580 assert_eq!(names.len(), 3);
581 assert!(names.contains(&"example.com".to_string()));
582 assert!(names.contains(&"www.example.com".to_string()));
583 assert!(names.contains(&"test.com".to_string()));
584 }
585
586 #[test]
587 fn test_summary() {
588 let config = r"
589 user nginx;
590 access_log /var/log/nginx/access.log;
591 server { listen 80; }
592 ";
593
594 let discovery = NginxDiscovery::from_config_text(config).unwrap();
595 let summary = discovery.summary();
596 assert!(summary.contains("directives"));
597 assert!(summary.contains("Server blocks: 1"));
598 }
599
600 #[test]
601 fn test_config_access() {
602 let config = "user nginx;";
603 let discovery = NginxDiscovery::from_config_text(config).unwrap();
604 let ast = discovery.config();
605 assert_eq!(ast.directives.len(), 1);
606 }
607
608 #[test]
609 #[cfg(feature = "serde")]
610 fn test_to_json() {
611 let config = "user nginx;";
612 let discovery = NginxDiscovery::from_config_text(config).unwrap();
613 let json = discovery.to_json().unwrap();
614 assert!(json.contains("user"));
615 }
616
617 #[test]
618 #[cfg(feature = "serde")]
619 fn test_to_yaml() {
620 let config = "user nginx;";
621 let discovery = NginxDiscovery::from_config_text(config).unwrap();
622 let yaml = discovery.to_yaml().unwrap();
623 assert!(yaml.contains("user"));
624 }
625}