1use std::env;
4use std::fmt::Debug;
5use std::net::SocketAddr;
6#[cfg(unix)]
7use std::os::unix::fs::MetadataExt;
8use std::path::PathBuf;
9use std::sync::atomic::AtomicBool;
10use std::sync::atomic::Ordering;
11use std::time::Duration;
12
13use anyhow::Result;
14use anyhow::anyhow;
15use clap::ArgAction;
16use clap::Args;
17use clap::CommandFactory;
18use clap::Parser;
19use clap::Subcommand;
20use clap::builder::FalseyValueParser;
21#[cfg(any(feature = "dhat-heap", feature = "dhat-ad-hoc"))]
22use once_cell::sync::OnceCell;
23use regex::Captures;
24use regex::Regex;
25use url::ParseError;
26use url::Url;
27
28use crate::LicenseSource;
29use crate::configuration::Discussed;
30use crate::configuration::generate_config_schema;
31use crate::configuration::generate_upgrade;
32use crate::metrics::meter_provider;
33use crate::plugin::plugins;
34use crate::plugins::telemetry::reload::init_telemetry;
35use crate::router::ConfigurationSource;
36use crate::router::RouterHttpServer;
37use crate::router::SchemaSource;
38use crate::router::ShutdownSource;
39use crate::uplink::Endpoints;
40use crate::uplink::UplinkConfig;
41
42#[cfg(all(
43 feature = "global-allocator",
44 not(feature = "dhat-heap"),
45 target_os = "linux"
46))]
47#[global_allocator]
48static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
49
50#[cfg(feature = "dhat-heap")]
54#[global_allocator]
55pub(crate) static ALLOC: dhat::Alloc = dhat::Alloc;
56
57#[cfg(feature = "dhat-heap")]
58pub(crate) static mut DHAT_HEAP_PROFILER: OnceCell<dhat::Profiler> = OnceCell::new();
59
60#[cfg(feature = "dhat-ad-hoc")]
61pub(crate) static mut DHAT_AD_HOC_PROFILER: OnceCell<dhat::Profiler> = OnceCell::new();
62
63pub(crate) const APOLLO_ROUTER_DEV_ENV: &str = "APOLLO_ROUTER_DEV";
64pub(crate) const APOLLO_TELEMETRY_DISABLED: &str = "APOLLO_TELEMETRY_DISABLED";
65
66#[cfg(feature = "dhat-heap")]
69fn create_heap_profiler() {
70 unsafe {
71 match DHAT_HEAP_PROFILER.set(dhat::Profiler::new_heap()) {
72 Ok(p) => {
73 println!("heap profiler installed: {:?}", p);
74 libc::atexit(drop_heap_profiler);
75 }
76 Err(e) => eprintln!("heap profiler install failed: {:?}", e),
77 }
78 }
79}
80
81#[cfg(feature = "dhat-heap")]
82#[no_mangle]
83extern "C" fn drop_heap_profiler() {
84 unsafe {
85 if let Some(p) = DHAT_HEAP_PROFILER.take() {
86 drop(p);
87 }
88 }
89}
90
91#[cfg(feature = "dhat-ad-hoc")]
92fn create_ad_hoc_profiler() {
93 unsafe {
94 match DHAT_AD_HOC_PROFILER.set(dhat::Profiler::new_ad_hoc()) {
95 Ok(p) => {
96 println!("ad-hoc profiler installed: {:?}", p);
97 libc::atexit(drop_ad_hoc_profiler);
98 }
99 Err(e) => eprintln!("ad-hoc profiler install failed: {:?}", e),
100 }
101 }
102}
103
104#[cfg(feature = "dhat-ad-hoc")]
105#[no_mangle]
106extern "C" fn drop_ad_hoc_profiler() {
107 unsafe {
108 if let Some(p) = DHAT_AD_HOC_PROFILER.take() {
109 drop(p);
110 }
111 }
112}
113
114#[derive(Subcommand, Debug)]
116enum Commands {
117 Config(ConfigSubcommandArgs),
119}
120
121#[derive(Args, Debug)]
122struct ConfigSubcommandArgs {
123 #[clap(subcommand)]
125 command: ConfigSubcommand,
126}
127
128#[derive(Subcommand, Debug)]
129enum ConfigSubcommand {
130 Schema,
132
133 Upgrade {
135 #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
137 config_path: PathBuf,
138
139 #[clap(action = ArgAction::SetTrue, long)]
141 diff: bool,
142 },
143 Experimental,
145 Preview,
147}
148
149#[derive(Parser, Debug)]
151#[clap(name = "router", about = "Apollo federation router")]
152#[command(disable_version_flag(true))]
153pub struct Opt {
154 #[clap(
156 long = "log",
157 default_value = "info",
158 alias = "log-level",
159 value_parser = add_log_filter,
160 env = "APOLLO_ROUTER_LOG"
161 )]
162 log_level: String,
164
165 #[clap(
167 alias = "hr",
168 long = "hot-reload",
169 env = "APOLLO_ROUTER_HOT_RELOAD",
170 action(ArgAction::SetTrue)
171 )]
172 hot_reload: bool,
173
174 #[clap(
176 short,
177 long = "config",
178 value_parser,
179 env = "APOLLO_ROUTER_CONFIG_PATH"
180 )]
181 config_path: Option<PathBuf>,
182
183 #[clap(
185 env = APOLLO_ROUTER_DEV_ENV,
186 long = "dev",
187 action(ArgAction::SetTrue)
188 )]
189 dev: bool,
190
191 #[clap(
193 short,
194 long = "supergraph",
195 value_parser,
196 env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
197 )]
198 supergraph_path: Option<PathBuf>,
199
200 #[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
202 supergraph_urls: Option<Vec<Url>>,
203
204 #[clap(long, action(ArgAction::SetTrue), hide(true))]
206 schema: bool,
207
208 #[clap(subcommand)]
210 command: Option<Commands>,
211
212 #[clap(skip = std::env::var("APOLLO_KEY").ok())]
214 apollo_key: Option<String>,
215
216 #[cfg(unix)]
218 #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
219 apollo_key_path: Option<PathBuf>,
220
221 #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
223 apollo_graph_ref: Option<String>,
224
225 #[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
227 apollo_router_license: Option<String>,
228
229 #[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
231 apollo_router_license_path: Option<PathBuf>,
232
233 #[clap(long, env, action = ArgAction::Append)]
235 apollo_uplink_endpoints: Option<String>,
237
238 #[clap(long, default_value = "10s", value_parser = humantime::parse_duration, env)]
240 apollo_uplink_poll_interval: Duration,
241
242 #[clap(long, env = APOLLO_TELEMETRY_DISABLED, value_parser = FalseyValueParser::new())]
244 anonymous_telemetry_disabled: bool,
245
246 #[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
248 apollo_uplink_timeout: Duration,
249
250 #[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
252 listen_address: Option<SocketAddr>,
253
254 #[clap(action = ArgAction::SetTrue, long, short = 'V')]
256 pub(crate) version: bool,
257}
258
259fn add_log_filter(raw: &str) -> Result<String, String> {
263 match std::env::var("RUST_LOG") {
264 Ok(filter) => Ok(filter),
265 Err(_e) => {
266 let lowered = raw.to_lowercase();
268 let rgx =
270 Regex::new(r"(^|,)(off|error|warn|info|debug|trace)").expect("regex must be valid");
271 let res = rgx.replace_all(&lowered, |caps: &Captures| {
272 format!("{}apollo_router={}", &caps[1], &caps[2])
275 });
276 Ok(format!("info,{res}"))
277 }
278 }
279}
280
281impl Opt {
282 pub(crate) fn uplink_config(&self) -> Result<UplinkConfig, anyhow::Error> {
283 Ok(UplinkConfig {
284 apollo_key: self
285 .apollo_key
286 .clone()
287 .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
288 apollo_graph_ref: self
289 .apollo_graph_ref
290 .clone()
291 .ok_or(Self::err_require_opt("APOLLO_GRAPH_REF"))?,
292 endpoints: self
293 .apollo_uplink_endpoints
294 .as_ref()
295 .map(|endpoints| Self::parse_endpoints(endpoints))
296 .transpose()?,
297 poll_interval: self.apollo_uplink_poll_interval,
298 timeout: self.apollo_uplink_timeout,
299 })
300 }
301
302 pub(crate) fn is_telemetry_disabled(&self) -> bool {
303 self.anonymous_telemetry_disabled
304 }
305
306 fn parse_endpoints(endpoints: &str) -> std::result::Result<Endpoints, anyhow::Error> {
307 Ok(Endpoints::fallback(
308 endpoints
309 .split(',')
310 .map(|endpoint| Url::parse(endpoint.trim()))
311 .collect::<Result<Vec<Url>, ParseError>>()
312 .map_err(|err| anyhow!("invalid Apollo Uplink endpoint, {}", err))?,
313 ))
314 }
315
316 fn err_require_opt(env_var: &str) -> anyhow::Error {
317 anyhow!("Use of Apollo Graph OS requires setting the {env_var} environment variable")
318 }
319}
320
321pub fn main() -> Result<()> {
328 #[cfg(feature = "dhat-heap")]
329 create_heap_profiler();
330
331 #[cfg(feature = "dhat-ad-hoc")]
332 create_ad_hoc_profiler();
333
334 let mut builder = tokio::runtime::Builder::new_multi_thread();
335 builder.enable_all();
336
337 if let Some(nb) = std::env::var("APOLLO_ROUTER_IO_THREADS")
340 .ok()
341 .and_then(|value| value.parse::<usize>().ok())
342 {
343 builder.worker_threads(nb);
344 }
345
346 let runtime = builder.build()?;
347 runtime.block_on(Executable::builder().start())
348}
349
350#[non_exhaustive]
352pub struct Executable {}
353
354#[buildstructor::buildstructor]
355impl Executable {
356 #[builder(entry = "builder", exit = "start", visibility = "pub")]
398 async fn start(
399 shutdown: Option<ShutdownSource>,
400 schema: Option<SchemaSource>,
401 license: Option<LicenseSource>,
402 config: Option<ConfigurationSource>,
403 cli_args: Option<Opt>,
404 ) -> Result<()> {
405 let opt = cli_args.unwrap_or_else(Opt::parse);
406
407 if opt.version {
408 println!("{}", std::env!("CARGO_PKG_VERSION"));
409 return Ok(());
410 }
411
412 copy_args_to_env();
413
414 let apollo_telemetry_initialized = if graph_os() {
415 init_telemetry(&opt.log_level)?;
416 true
417 } else {
418 init_telemetry(&opt.log_level).is_ok()
420 };
421
422 setup_panic_handler();
423
424 if opt.schema {
425 eprintln!("`router --schema` is deprecated. Use `router config schema`");
426 let schema = generate_config_schema();
427 println!("{}", serde_json::to_string_pretty(&schema)?);
428 return Ok(());
429 }
430
431 let result = match opt.command.as_ref() {
432 Some(Commands::Config(ConfigSubcommandArgs {
433 command: ConfigSubcommand::Schema,
434 })) => {
435 let schema = generate_config_schema();
436 println!("{}", serde_json::to_string_pretty(&schema)?);
437 Ok(())
438 }
439 Some(Commands::Config(ConfigSubcommandArgs {
440 command: ConfigSubcommand::Upgrade { config_path, diff },
441 })) => {
442 let config_string = std::fs::read_to_string(config_path)?;
443 let output = generate_upgrade(&config_string, *diff)?;
444 println!("{output}");
445 Ok(())
446 }
447 Some(Commands::Config(ConfigSubcommandArgs {
448 command: ConfigSubcommand::Experimental,
449 })) => {
450 Discussed::new().print_experimental();
451 Ok(())
452 }
453 Some(Commands::Config(ConfigSubcommandArgs {
454 command: ConfigSubcommand::Preview,
455 })) => {
456 Discussed::new().print_preview();
457 Ok(())
458 }
459 None => Self::inner_start(shutdown, schema, config, license, opt).await,
460 };
461
462 if apollo_telemetry_initialized {
463 tokio::task::spawn_blocking(move || {
465 opentelemetry::global::shutdown_tracer_provider();
466 meter_provider().shutdown();
467 })
468 .await?;
469 }
470 result
471 }
472
473 async fn inner_start(
474 shutdown: Option<ShutdownSource>,
475 schema: Option<SchemaSource>,
476 config: Option<ConfigurationSource>,
477 license: Option<LicenseSource>,
478 mut opt: Opt,
479 ) -> Result<()> {
480 if opt.apollo_uplink_poll_interval < Duration::from_secs(10) {
481 return Err(anyhow!("apollo-uplink-poll-interval must be at least 10s"));
482 }
483 let current_directory = std::env::current_dir()?;
484 opt.hot_reload = opt.hot_reload || opt.dev;
486
487 let configuration = match (config, opt.config_path.as_ref()) {
488 (Some(_), Some(_)) => {
489 return Err(anyhow!(
490 "--config and APOLLO_ROUTER_CONFIG_PATH cannot be used when a custom configuration source is in use"
491 ));
492 }
493 (Some(config), None) => config,
494 #[allow(clippy::blocks_in_conditions)]
495 _ => opt
496 .config_path
497 .as_ref()
498 .map(|path| {
499 let path = if path.is_relative() {
500 current_directory.join(path)
501 } else {
502 path.to_path_buf()
503 };
504
505 ConfigurationSource::File {
506 path,
507 watch: opt.hot_reload,
508 delay: None,
509 }
510 })
511 .unwrap_or_default(),
512 };
513
514 let apollo_telemetry_msg = if opt.anonymous_telemetry_disabled {
515 "Anonymous usage data collection is disabled.".to_string()
516 } else {
517 "Anonymous usage data is gathered to inform Apollo product development. See https://go.apollo.dev/o/privacy for details.".to_string()
518 };
519
520 let apollo_router_msg = format!(
521 "Apollo Router v{} // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)",
522 std::env!("CARGO_PKG_VERSION")
523 );
524
525 #[cfg(unix)]
531 let akp = &opt.apollo_key_path;
532 #[cfg(not(unix))]
533 let akp: &Option<PathBuf> = &None;
534
535 let schema_source = match (
536 schema,
537 &opt.supergraph_path,
538 &opt.supergraph_urls,
539 &opt.apollo_key,
540 akp,
541 ) {
542 (Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => {
543 return Err(anyhow!(
544 "--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use"
545 ));
546 }
547 (Some(source), None, None, _, _) => source,
548 (_, Some(supergraph_path), _, _, _) => {
549 tracing::info!("{apollo_router_msg}");
550 tracing::info!("{apollo_telemetry_msg}");
551
552 let supergraph_path = if supergraph_path.is_relative() {
553 current_directory.join(supergraph_path)
554 } else {
555 supergraph_path.clone()
556 };
557 SchemaSource::File {
558 path: supergraph_path,
559 watch: opt.hot_reload,
560 delay: None,
561 }
562 }
563 (_, _, Some(supergraph_urls), _, _) => {
564 tracing::info!("{apollo_router_msg}");
565 tracing::info!("{apollo_telemetry_msg}");
566
567 SchemaSource::URLs {
568 urls: supergraph_urls.clone(),
569 watch: opt.hot_reload,
570 period: opt.apollo_uplink_poll_interval,
571 }
572 }
573 (_, None, None, _, Some(apollo_key_path)) => {
574 let apollo_key_path = if apollo_key_path.is_relative() {
575 current_directory.join(apollo_key_path)
576 } else {
577 apollo_key_path.clone()
578 };
579
580 if !apollo_key_path.exists() {
581 tracing::error!(
582 "Apollo key at path '{}' does not exist.",
583 apollo_key_path.to_string_lossy()
584 );
585 return Err(anyhow!(
586 "Apollo key at path '{}' does not exist.",
587 apollo_key_path.to_string_lossy()
588 ));
589 } else {
590 #[cfg(unix)]
594 {
595 let meta = std::fs::metadata(apollo_key_path.clone())
596 .map_err(|err| anyhow!("Failed to read Apollo key file: {}", err))?;
597 let mode = meta.mode();
598 if mode & 0o077 != 0 {
601 return Err(anyhow!(
602 "Apollo key file permissions ({:#o}) are too permissive",
603 mode & 0o000777
604 ));
605 }
606 let euid = unsafe { libc::geteuid() };
607 let owner = meta.uid();
608 if euid != owner {
609 return Err(anyhow!(
610 "Apollo key file owner id ({owner}) does not match effective user id ({euid})"
611 ));
612 }
613 }
614 match std::fs::read_to_string(&apollo_key_path) {
616 Ok(apollo_key) => {
617 opt.apollo_key = Some(apollo_key.trim().to_string());
618 }
619 Err(err) => {
620 return Err(anyhow!("Failed to read Apollo key file: {}", err));
621 }
622 };
623 SchemaSource::Registry(opt.uplink_config()?)
624 }
625 }
626 (_, None, None, Some(_apollo_key), None) => {
627 tracing::info!("{apollo_router_msg}");
628 tracing::info!("{apollo_telemetry_msg}");
629 SchemaSource::Registry(opt.uplink_config()?)
630 }
631 _ => {
632 return Err(anyhow!(
633 r#"{apollo_router_msg}
634
635⚠️ The Apollo Router requires a composed supergraph schema at startup. ⚠️
636
637👉 DO ONE:
638
639 * Pass a local schema file with the '--supergraph' option:
640
641 $ ./router --supergraph <file_path>
642
643 * Fetch a registered schema from GraphOS by setting
644 these environment variables:
645
646 $ APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router
647
648 For details, see the Apollo docs:
649 https://www.apollographql.com/docs/federation/managed-federation/setup
650
651🔬 TESTING THINGS OUT?
652
653 1. Download an example supergraph schema with Apollo-hosted subgraphs:
654
655 $ curl -L https://supergraph.demo.starstuff.dev/ > starstuff.graphql
656
657 2. Run the Apollo Router in development mode with the supergraph schema:
658
659 $ ./router --dev --supergraph starstuff.graphql
660
661 "#
662 ));
663 }
664 };
665
666 let license = if let Some(license) = license {
672 license
673 } else {
674 match (
675 &opt.apollo_router_license,
676 &opt.apollo_router_license_path,
677 &opt.apollo_key,
678 &opt.apollo_graph_ref,
679 ) {
680 (_, Some(license_path), _, _) => {
681 let license_path = if license_path.is_relative() {
682 current_directory.join(license_path)
683 } else {
684 license_path.clone()
685 };
686 LicenseSource::File {
687 path: license_path,
688 watch: opt.hot_reload,
689 }
690 }
691 (Some(_license), _, _, _) => LicenseSource::Env,
692 (_, _, Some(_apollo_key), Some(_apollo_graph_ref)) => {
693 LicenseSource::Registry(opt.uplink_config()?)
694 }
695
696 _ => LicenseSource::default(),
697 }
698 };
699
700 let user_plugins_present = plugins().filter(|p| !p.is_apollo()).count() > 0;
702 let rust_log_set = std::env::var("RUST_LOG").is_ok();
703 let apollo_router_log = std::env::var("APOLLO_ROUTER_LOG").unwrap_or_default();
704 if user_plugins_present
705 && !rust_log_set
706 && ["trace", "debug", "warn", "error", "info"].contains(&apollo_router_log.as_str())
707 {
708 tracing::info!(
709 "Custom plugins are present. To see log messages from your plugins you must configure `RUST_LOG` or `APOLLO_ROUTER_LOG` environment variables. See the Router logging documentation for more details"
710 );
711 }
712
713 let uplink_config = opt.uplink_config().ok();
714 if uplink_config
715 .clone()
716 .unwrap_or_default()
717 .endpoints
718 .unwrap_or_default()
719 .url_count()
720 == 1
721 {
722 tracing::warn!(
723 "Only a single uplink endpoint is configured. We recommend specifying at least two endpoints so that a fallback exists."
724 );
725 }
726
727 let router = RouterHttpServer::builder()
728 .is_telemetry_disabled(opt.is_telemetry_disabled())
729 .configuration(configuration)
730 .and_uplink(uplink_config)
731 .schema(schema_source)
732 .license(license)
733 .shutdown(shutdown.unwrap_or(ShutdownSource::CtrlC))
734 .start();
735
736 if let Err(err) = router.await {
737 tracing::error!("{}", err);
738 return Err(err.into());
739 }
740 Ok(())
741 }
742}
743
744fn graph_os() -> bool {
745 std::env::var("APOLLO_KEY").is_ok() && std::env::var("APOLLO_GRAPH_REF").is_ok()
746}
747
748fn setup_panic_handler() {
749 let backtrace_env = std::env::var("RUST_BACKTRACE");
751 let show_backtraces =
752 backtrace_env.as_deref() == Ok("1") || backtrace_env.as_deref() == Ok("full");
753 if show_backtraces {
754 tracing::warn!(
755 "RUST_BACKTRACE={} detected. This is useful for diagnostics but will have a performance impact and may leak sensitive information",
756 backtrace_env.as_ref().unwrap()
757 );
758 }
759 std::panic::set_hook(Box::new(move |e| {
760 if show_backtraces {
761 let backtrace = std::backtrace::Backtrace::capture();
762 tracing::error!("{}\n{}", e, backtrace)
763 } else {
764 tracing::error!("{}", e)
765 }
766
767 std::process::exit(1);
770 }));
771}
772
773static COPIED: AtomicBool = AtomicBool::new(false);
774
775fn copy_args_to_env() {
776 if Ok(false) != COPIED.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) {
777 panic!(
778 "`copy_args_to_env` was called twice: That means `Executable::start` was called twice in the same process, which should not happen"
779 );
780 }
781 let matches = Opt::command().get_matches();
786 Opt::command().get_arguments().for_each(|a| {
787 if let Some(env) = a.get_env() {
788 if let Some(raw) = matches
789 .get_raw(a.get_id().as_str())
790 .unwrap_or_default()
791 .next()
792 {
793 env::set_var(env, raw);
794 }
795 }
796 });
797}
798
799#[cfg(test)]
800mod tests {
801 use crate::executable::add_log_filter;
802
803 #[test]
804 fn simplest_logging_modifications() {
805 for level in ["off", "error", "warn", "info", "debug", "trace"] {
806 assert_eq!(
807 add_log_filter(level).expect("conversion works"),
808 format!("info,apollo_router={level}")
809 );
810 }
811 }
812
813 #[test]
818 fn complex_logging_modifications() {
819 assert_eq!(add_log_filter("hello").unwrap(), "info,hello");
820 assert_eq!(add_log_filter("trace").unwrap(), "info,apollo_router=trace");
821 assert_eq!(add_log_filter("TRACE").unwrap(), "info,apollo_router=trace");
822 assert_eq!(add_log_filter("info").unwrap(), "info,apollo_router=info");
823 assert_eq!(add_log_filter("INFO").unwrap(), "info,apollo_router=info");
824 assert_eq!(add_log_filter("hello=debug").unwrap(), "info,hello=debug");
825 assert_eq!(add_log_filter("hello=DEBUG").unwrap(), "info,hello=debug");
826 assert_eq!(
827 add_log_filter("hello,std::option").unwrap(),
828 "info,hello,std::option"
829 );
830 assert_eq!(
831 add_log_filter("error,hello=warn").unwrap(),
832 "info,apollo_router=error,hello=warn"
833 );
834 assert_eq!(
835 add_log_filter("error,hello=off").unwrap(),
836 "info,apollo_router=error,hello=off"
837 );
838 assert_eq!(add_log_filter("off").unwrap(), "info,apollo_router=off");
839 assert_eq!(add_log_filter("OFF").unwrap(), "info,apollo_router=off");
840 assert_eq!(add_log_filter("hello/foo").unwrap(), "info,hello/foo");
841 assert_eq!(add_log_filter("hello/f.o").unwrap(), "info,hello/f.o");
842 assert_eq!(
843 add_log_filter("hello=debug/foo*foo").unwrap(),
844 "info,hello=debug/foo*foo"
845 );
846 assert_eq!(
847 add_log_filter("error,hello=warn/[0-9]scopes").unwrap(),
848 "info,apollo_router=error,hello=warn/[0-9]scopes"
849 );
850 assert_eq!(
852 add_log_filter("hyper=debug,warn,regex=warn,h2=off").unwrap(),
853 "info,hyper=debug,apollo_router=warn,regex=warn,h2=off"
854 );
855 assert_eq!(
856 add_log_filter("hyper=debug,apollo_router=off,regex=info,h2=off").unwrap(),
857 "info,hyper=debug,apollo_router=off,regex=info,h2=off"
858 );
859 assert_eq!(
860 add_log_filter("apollo_router::plugins=debug").unwrap(),
861 "info,apollo_router::plugins=debug"
862 );
863 }
864}