Skip to main content

apollo_router/
executable.rs

1//! Main entry point for CLI command to start server.
2
3use 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// Note: the dhat-heap and dhat-ad-hoc features should not be both enabled. We name our functions
51// and variables identically to prevent this from happening.
52
53#[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// Note: Constructor/Destructor functions may not play nicely with tracing, since they run after
67// main completes, so don't use tracing, use println!() and eprintln!()..
68#[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/// Subcommands
115#[derive(Subcommand, Debug)]
116enum Commands {
117    /// Configuration subcommands.
118    Config(ConfigSubcommandArgs),
119}
120
121#[derive(Args, Debug)]
122struct ConfigSubcommandArgs {
123    /// Subcommands
124    #[clap(subcommand)]
125    command: ConfigSubcommand,
126}
127
128#[derive(Subcommand, Debug)]
129enum ConfigSubcommand {
130    /// Print the json configuration schema.
131    Schema,
132
133    /// Print upgraded configuration.
134    Upgrade {
135        /// The location of the config to upgrade.
136        #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
137        config_path: PathBuf,
138
139        /// Print a diff.
140        #[clap(action = ArgAction::SetTrue, long)]
141        diff: bool,
142    },
143    /// List all the available experimental configurations with related GitHub discussion
144    Experimental,
145    /// List all the available preview configurations with related GitHub discussion
146    Preview,
147}
148
149/// Options for the router
150#[derive(Parser, Debug)]
151#[clap(name = "router", about = "Apollo federation router")]
152#[command(disable_version_flag(true))]
153pub struct Opt {
154    /// Log level (off|error|warn|info|debug|trace).
155    #[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    // FIXME: when upgrading to router 2.0 we should put this value in an Option
163    log_level: String,
164
165    /// Reload locally provided configuration and supergraph files automatically.  This only affects watching of local files and does not affect supergraphs and configuration provided by GraphOS through Uplink, which is always reloaded immediately.
166    #[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    /// Configuration location relative to the project directory.
175    #[clap(
176        short,
177        long = "config",
178        value_parser,
179        env = "APOLLO_ROUTER_CONFIG_PATH"
180    )]
181    config_path: Option<PathBuf>,
182
183    /// Enable development mode.
184    #[clap(
185        env = APOLLO_ROUTER_DEV_ENV,
186        long = "dev",
187        action(ArgAction::SetTrue)
188    )]
189    dev: bool,
190
191    /// Schema location relative to the project directory.
192    #[clap(
193        short,
194        long = "supergraph",
195        value_parser,
196        env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
197    )]
198    supergraph_path: Option<PathBuf>,
199
200    /// Locations (comma separated) to fetch the supergraph from. These will be queried in order.
201    #[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
202    supergraph_urls: Option<Vec<Url>>,
203
204    /// Prints the configuration schema.
205    #[clap(long, action(ArgAction::SetTrue), hide(true))]
206    schema: bool,
207
208    /// Subcommands
209    #[clap(subcommand)]
210    command: Option<Commands>,
211
212    /// Your Apollo key.
213    #[clap(skip = std::env::var("APOLLO_KEY").ok())]
214    apollo_key: Option<String>,
215
216    /// Key file location relative to the current directory.
217    #[cfg(unix)]
218    #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
219    apollo_key_path: Option<PathBuf>,
220
221    /// Your Apollo graph reference.
222    #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
223    apollo_graph_ref: Option<String>,
224
225    /// Your Apollo Router license.
226    #[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
227    apollo_router_license: Option<String>,
228
229    /// License location relative to the current directory.
230    #[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
231    apollo_router_license_path: Option<PathBuf>,
232
233    /// The endpoints (comma separated) polled to fetch the latest supergraph schema.
234    #[clap(long, env, action = ArgAction::Append)]
235    // Should be a Vec<Url> when https://github.com/clap-rs/clap/discussions/3796 is solved
236    apollo_uplink_endpoints: Option<String>,
237
238    /// The time between polls to Apollo uplink. Minimum 10s.
239    #[clap(long, default_value = "10s", value_parser = humantime::parse_duration, env)]
240    apollo_uplink_poll_interval: Duration,
241
242    /// Disable sending anonymous usage information to Apollo.
243    #[clap(long, env = APOLLO_TELEMETRY_DISABLED, value_parser = FalseyValueParser::new())]
244    anonymous_telemetry_disabled: bool,
245
246    /// The timeout for an http call to Apollo uplink. Defaults to 30s.
247    #[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
248    apollo_uplink_timeout: Duration,
249
250    /// The listen address for the router. Overrides `supergraph.listen` in router.yaml.
251    #[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
252    listen_address: Option<SocketAddr>,
253
254    /// Display version and exit.
255    #[clap(action = ArgAction::SetTrue, long, short = 'V')]
256    pub(crate) version: bool,
257}
258
259// Add a filter to global log level settings so that the level only applies to the router.
260//
261// If you want to set a complex logging filter which isn't modified in this way, use RUST_LOG.
262fn add_log_filter(raw: &str) -> Result<String, String> {
263    match std::env::var("RUST_LOG") {
264        Ok(filter) => Ok(filter),
265        Err(_e) => {
266            // Directives are case-insensitive. Convert to lowercase before processing.
267            let lowered = raw.to_lowercase();
268            // Find "global" directives and limit them to apollo_router
269            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                // The default level is info, then other ones can override the default one
273                // If the pattern matches, we must have caps 1 and 2
274                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
321/// This is the main router entrypoint.
322///
323/// Starts a Tokio runtime and runs a Router in it based on command-line options.
324/// Returns on fatal error or after graceful shutdown has completed.
325///
326/// Refer to the examples if you would like to see how to run your own router with plugins.
327pub 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    // This environment variable is intentionally undocumented.
338    // See also APOLLO_ROUTER_COMPUTE_THREADS in apollo-router/src/compute_job.rs
339    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/// Entry point into creating a router executable with more customization than [`main`].
351#[non_exhaustive]
352pub struct Executable {}
353
354#[buildstructor::buildstructor]
355impl Executable {
356    /// Returns a builder that can parse command-line options and run a Router
357    /// in an existing Tokio runtime.
358    ///
359    /// Builder methods:
360    ///
361    /// * `.config(impl Into<`[`ConfigurationSource`]`>)`
362    ///   Optional.
363    ///   Specifies where to find the Router configuration.
364    ///   The default is the file specified by the `--config` or `-c` CLI option.
365    ///
366    /// * `.schema(impl Into<`[`SchemaSource`]`>)`
367    ///   Optional.
368    ///   Specifies when to find the supergraph schema.
369    ///   The default is the file specified by the `--supergraph` or `-s` CLI option.
370    ///
371    /// * `.shutdown(impl Into<`[`ShutdownSource`]`>)`
372    ///   Optional.
373    ///   Specifies when the Router should shut down gracefully.
374    ///   The default is on CTRL+C (`SIGINT`).
375    ///
376    /// * `.start()`
377    ///   Returns a future that resolves to [`anyhow::Result`]`<()>`
378    ///   on fatal error or after graceful shutdown has completed.
379    ///   Must be called (and the future awaited) in the context of an existing Tokio runtime.
380    ///
381    /// ```no_run
382    /// use apollo_router::{Executable, ShutdownSource};
383    /// # #[tokio::main]
384    /// # async fn main() -> anyhow::Result<()> {
385    /// # use futures::StreamExt;
386    /// # let schemas = futures::stream::empty().boxed();
387    /// # let configs = futures::stream::empty().boxed();
388    /// use apollo_router::{ConfigurationSource, SchemaSource};
389    /// Executable::builder()
390    ///   .shutdown(ShutdownSource::None)
391    ///   .schema(SchemaSource::Stream(schemas))
392    ///   .config(ConfigurationSource::Stream(configs))
393    ///   .start()
394    ///   .await
395    /// # }
396    /// ```
397    #[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            // Best effort init telemetry
419            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            // We should be good to shutdown OpenTelemetry now as the router should have finished everything.
464            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        // Enable hot reload when dev mode is enabled
485        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        // Schema source will be in order of precedence:
526        // 1. Cli --supergraph
527        // 2. Env APOLLO_ROUTER_SUPERGRAPH_PATH
528        // 3. Env APOLLO_ROUTER_SUPERGRAPH_URLS
529        // 4. Env APOLLO_KEY and APOLLO_GRAPH_REF
530        #[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                    // On unix systems, Check that the executing user is the only user who may
591                    // read the key file.
592                    // Note: We could, in future, add support for Windows.
593                    #[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 our mode isn't "safe", fail...
599                        // safe == none of the "group" or "other" bits set.
600                        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                    //The key file exists try and load it
615                    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        // Order of precedence for licenses:
667        // 1. explicit path from cli
668        // 2. env APOLLO_ROUTER_LICENSE
669        // 3. uplink
670
671        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        // If there are custom plugins then if RUST_LOG hasn't been set and APOLLO_ROUTER_LOG contains one of the defaults.
701        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    // Redirect panics to the logs.
750    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        // Once we've panic'ed the behaviour of the router is non-deterministic
768        // We've logged out the panic details. Terminate with an error code
769        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    // Copy all the args to env.
782    // This way, Clap is still responsible for the definitive view of what the current options are.
783    // But if we have code that relies on env variable then it will still work.
784    // Env variables should disappear over time as we move to plugins.
785    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    // It's hard to have comprehensive tests for this kind of functionality,
814    // so this set is derived from the examples at:
815    // https://docs.rs/env_logger/latest/env_logger/#filtering-results
816    // which is a reasonably corpus of things to test.
817    #[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        // Add some hard ones
851        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}