apollo_router/
executable.rs

1//! Main entry point for CLI command to start server.
2
3use std::fmt::Debug;
4use std::net::SocketAddr;
5#[cfg(unix)]
6use std::os::unix::fs::MetadataExt;
7use std::path::PathBuf;
8use std::sync::atomic::AtomicBool;
9use std::sync::atomic::Ordering;
10use std::time::Duration;
11
12use anyhow::Result;
13use anyhow::anyhow;
14use clap::ArgAction;
15use clap::Args;
16use clap::Parser;
17use clap::Subcommand;
18use clap::builder::FalseyValueParser;
19use parking_lot::Mutex;
20use regex::Captures;
21use regex::Regex;
22use url::ParseError;
23use url::Url;
24
25use crate::LicenseSource;
26use crate::configuration::Discussed;
27use crate::configuration::expansion::Expansion;
28use crate::configuration::generate_config_schema;
29use crate::configuration::generate_upgrade;
30use crate::configuration::schema::Mode;
31use crate::configuration::validate_yaml_configuration;
32use crate::metrics::meter_provider_internal;
33use crate::plugin::plugins;
34use crate::plugins::telemetry::reload::otel::init_telemetry;
35use crate::registry::OciConfig;
36use crate::router::ConfigurationSource;
37use crate::router::RouterHttpServer;
38use crate::router::SchemaSource;
39use crate::router::ShutdownSource;
40use crate::uplink::Endpoints;
41use crate::uplink::UplinkConfig;
42
43pub(crate) static APOLLO_ROUTER_DEV_MODE: AtomicBool = AtomicBool::new(false);
44pub(crate) static APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
45pub(crate) static APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET: AtomicBool = AtomicBool::new(false);
46pub(crate) static APOLLO_ROUTER_LICENCE_IS_SET: AtomicBool = AtomicBool::new(false);
47pub(crate) static APOLLO_ROUTER_LICENCE_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
48pub(crate) static APOLLO_TELEMETRY_DISABLED: AtomicBool = AtomicBool::new(false);
49pub(crate) static APOLLO_ROUTER_LISTEN_ADDRESS: Mutex<Option<SocketAddr>> = Mutex::new(None);
50
51const INITIAL_UPLINK_POLL_INTERVAL: Duration = Duration::from_secs(10);
52
53/// Subcommands
54#[derive(Subcommand, Debug)]
55enum Commands {
56    /// Configuration subcommands.
57    Config(ConfigSubcommandArgs),
58}
59
60#[derive(Args, Debug)]
61struct ConfigSubcommandArgs {
62    /// Subcommands
63    #[clap(subcommand)]
64    command: ConfigSubcommand,
65}
66
67#[derive(Subcommand, Debug)]
68enum ConfigSubcommand {
69    /// Print the json configuration schema.
70    Schema,
71
72    /// Print upgraded configuration.
73    Upgrade {
74        /// The location of the config to upgrade.
75        #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
76        config_path: PathBuf,
77
78        /// Print a diff.
79        #[clap(action = ArgAction::SetTrue, long)]
80        diff: bool,
81    },
82    /// Validate existing Router configuration file
83    Validate {
84        /// The location of the config to validate.
85        #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
86        config_path: PathBuf,
87    },
88    /// List all the available experimental configurations with related GitHub discussion
89    Experimental,
90    /// List all the available preview configurations with related GitHub discussion
91    Preview,
92}
93
94/// Options for the router
95#[derive(Parser, Debug)]
96#[clap(name = "router", about = "Apollo federation router")]
97#[command(disable_version_flag(true))]
98pub struct Opt {
99    /// Log level (off|error|warn|info|debug|trace).
100    #[clap(
101        long = "log",
102        default_value = "info",
103        alias = "log-level",
104        value_parser = add_log_filter,
105        env = "APOLLO_ROUTER_LOG"
106    )]
107    // FIXME: when upgrading to router 2.0 we should put this value in an Option
108    log_level: String,
109
110    /// 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.
111    #[clap(
112        alias = "hr",
113        long = "hot-reload",
114        env = "APOLLO_ROUTER_HOT_RELOAD",
115        action(ArgAction::SetTrue)
116    )]
117    hot_reload: bool,
118
119    /// Configuration location relative to the project directory.
120    #[clap(
121        short,
122        long = "config",
123        value_parser,
124        env = "APOLLO_ROUTER_CONFIG_PATH"
125    )]
126    config_path: Option<PathBuf>,
127
128    /// Enable development mode.
129    #[clap(env = "APOLLO_ROUTER_DEV", long = "dev", action(ArgAction::SetTrue))]
130    dev: bool,
131
132    /// Schema location relative to the project directory.
133    #[clap(
134        short,
135        long = "supergraph",
136        value_parser,
137        env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
138    )]
139    supergraph_path: Option<PathBuf>,
140
141    /// Locations (comma separated) to fetch the supergraph from. These will be queried in order.
142    #[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
143    supergraph_urls: Option<Vec<Url>>,
144
145    /// Subcommands
146    #[clap(subcommand)]
147    command: Option<Commands>,
148
149    /// Your Apollo key.
150    #[clap(skip = std::env::var("APOLLO_KEY").ok())]
151    apollo_key: Option<String>,
152
153    /// Key file location relative to the current directory.
154    #[cfg(unix)]
155    #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
156    apollo_key_path: Option<PathBuf>,
157
158    /// Your Apollo graph reference.
159    #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
160    apollo_graph_ref: Option<String>,
161
162    /// Your Apollo Router license.
163    #[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
164    apollo_router_license: Option<String>,
165
166    /// License location relative to the current directory.
167    #[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
168    apollo_router_license_path: Option<PathBuf>,
169
170    /// The endpoints (comma separated) polled to fetch the latest supergraph schema.
171    #[clap(long, env, action = ArgAction::Append)]
172    // Should be a Vec<Url> when https://github.com/clap-rs/clap/discussions/3796 is solved
173    apollo_uplink_endpoints: Option<String>,
174
175    /// An OCI reference to a graph artifact that contains the supergraph schema for the router to run.
176    #[clap(long, env = "APOLLO_GRAPH_ARTIFACT_REFERENCE", action = ArgAction::Append)]
177    graph_artifact_reference: Option<String>,
178
179    /// Disable sending anonymous usage information to Apollo.
180    #[clap(long, env = "APOLLO_TELEMETRY_DISABLED", value_parser = FalseyValueParser::new())]
181    anonymous_telemetry_disabled: bool,
182
183    /// The timeout for an http call to Apollo uplink. Defaults to 30s.
184    #[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
185    apollo_uplink_timeout: Duration,
186
187    /// The listen address for the router. Overrides `supergraph.listen` in router.yaml.
188    #[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
189    listen_address: Option<SocketAddr>,
190
191    /// Display version and exit.
192    #[clap(action = ArgAction::SetTrue, long, short = 'V')]
193    pub(crate) version: bool,
194}
195
196// Add a filter to global log level settings so that the level only applies to the router.
197//
198// If you want to set a complex logging filter which isn't modified in this way, use RUST_LOG.
199fn add_log_filter(raw: &str) -> Result<String, String> {
200    match std::env::var("RUST_LOG") {
201        Ok(filter) => Ok(filter),
202        Err(_e) => {
203            // Directives are case-insensitive. Convert to lowercase before processing.
204            let lowered = raw.to_lowercase();
205            // Find "global" directives and limit them to apollo_router
206            let rgx =
207                Regex::new(r"(^|,)(off|error|warn|info|debug|trace)").expect("regex must be valid");
208            let res = rgx.replace_all(&lowered, |caps: &Captures| {
209                // The default level is info, then other ones can override the default one
210                // If the pattern matches, we must have caps 1 and 2
211                format!("{}apollo_router={}", &caps[1], &caps[2])
212            });
213            Ok(format!("info,{res}"))
214        }
215    }
216}
217
218impl Opt {
219    pub(crate) fn uplink_config(&self) -> Result<UplinkConfig, anyhow::Error> {
220        Ok(UplinkConfig {
221            apollo_key: self
222                .apollo_key
223                .clone()
224                .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
225            apollo_graph_ref: self
226                .apollo_graph_ref
227                .clone()
228                .ok_or(Self::err_require_opt("APOLLO_GRAPH_REF"))?,
229            endpoints: self
230                .apollo_uplink_endpoints
231                .as_ref()
232                .map(|endpoints| Self::parse_endpoints(endpoints))
233                .transpose()?,
234            poll_interval: INITIAL_UPLINK_POLL_INTERVAL,
235            timeout: self.apollo_uplink_timeout,
236        })
237    }
238
239    pub(crate) fn oci_config(&self) -> Result<OciConfig, anyhow::Error> {
240        Ok(OciConfig {
241            apollo_key: self
242                .apollo_key
243                .clone()
244                .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
245            reference: Self::validate_oci_reference(
246                &self
247                    .graph_artifact_reference
248                    .clone()
249                    .ok_or(Self::err_require_opt("APOLLO_GRAPH_ARTIFACT_REFERENCE"))?,
250            )?,
251        })
252    }
253
254    pub fn validate_oci_reference(reference: &str) -> std::result::Result<String, anyhow::Error> {
255        // Currently only shas are allowed to be passed as graph artifact references
256        // TODO Update when tag reloading is implemented
257        let valid_regex = Regex::new(r"@sha256:[0-9a-fA-F]{64}$").unwrap();
258        if valid_regex.is_match(reference) {
259            tracing::debug!("validated OCI configuration");
260            Ok(reference.to_string())
261        } else {
262            Err(anyhow!("invalid graph artifact reference: {reference}"))
263        }
264    }
265
266    fn parse_endpoints(endpoints: &str) -> std::result::Result<Endpoints, anyhow::Error> {
267        Ok(Endpoints::fallback(
268            endpoints
269                .split(',')
270                .map(|endpoint| Url::parse(endpoint.trim()))
271                .collect::<Result<Vec<Url>, ParseError>>()
272                .map_err(|err| anyhow!("invalid Apollo Uplink endpoint, {}", err))?,
273        ))
274    }
275
276    fn err_require_opt(env_var: &str) -> anyhow::Error {
277        anyhow!("Use of Apollo Graph OS requires setting the {env_var} environment variable")
278    }
279}
280
281/// This is the main router entrypoint.
282///
283/// Starts a Tokio runtime and runs a Router in it based on command-line options.
284/// Returns on fatal error or after graceful shutdown has completed.
285///
286/// Refer to the examples if you would like to see how to run your own router with plugins.
287pub fn main() -> Result<()> {
288    #[cfg(feature = "dhat-heap")]
289    crate::allocator::create_heap_profiler();
290
291    #[cfg(feature = "dhat-ad-hoc")]
292    crate::allocator::create_ad_hoc_profiler();
293
294    let mut builder = tokio::runtime::Builder::new_multi_thread();
295    builder.enable_all();
296
297    // This environment variable is intentionally undocumented.
298    // See also APOLLO_ROUTER_COMPUTE_THREADS in apollo-router/src/compute_job.rs
299    if let Some(nb) = std::env::var("APOLLO_ROUTER_IO_THREADS")
300        .ok()
301        .and_then(|value| value.parse::<usize>().ok())
302    {
303        builder.worker_threads(nb);
304    }
305
306    let runtime = builder.build()?;
307    runtime.block_on(Executable::builder().start())
308}
309
310/// Entry point into creating a router executable with more customization than [`main`].
311#[non_exhaustive]
312pub struct Executable {}
313
314#[buildstructor::buildstructor]
315impl Executable {
316    /// Returns a builder that can parse command-line options and run a Router
317    /// in an existing Tokio runtime.
318    ///
319    /// Builder methods:
320    ///
321    /// * `.config(impl Into<`[`ConfigurationSource`]`>)`
322    ///   Optional.
323    ///   Specifies where to find the Router configuration.
324    ///   The default is the file specified by the `--config` or `-c` CLI option.
325    ///
326    /// * `.schema(impl Into<`[`SchemaSource`]`>)`
327    ///   Optional.
328    ///   Specifies when to find the supergraph schema.
329    ///   The default is the file specified by the `--supergraph` or `-s` CLI option.
330    ///
331    /// * `.shutdown(impl Into<`[`ShutdownSource`]`>)`
332    ///   Optional.
333    ///   Specifies when the Router should shut down gracefully.
334    ///   The default is on CTRL+C (`SIGINT`).
335    ///
336    /// * `.start()`
337    ///   Returns a future that resolves to [`anyhow::Result`]`<()>`
338    ///   on fatal error or after graceful shutdown has completed.
339    ///   Must be called (and the future awaited) in the context of an existing Tokio runtime.
340    ///
341    /// ```no_run
342    /// use apollo_router::{Executable, ShutdownSource};
343    /// # #[tokio::main]
344    /// # async fn main() -> anyhow::Result<()> {
345    /// # use futures::StreamExt;
346    /// # let schemas = futures::stream::empty().boxed();
347    /// # let configs = futures::stream::empty().boxed();
348    /// use apollo_router::{ConfigurationSource, SchemaSource};
349    /// Executable::builder()
350    ///   .shutdown(ShutdownSource::None)
351    ///   .schema(SchemaSource::Stream(schemas))
352    ///   .config(ConfigurationSource::Stream(configs))
353    ///   .start()
354    ///   .await
355    /// # }
356    /// ```
357    #[builder(entry = "builder", exit = "start", visibility = "pub")]
358    async fn start(
359        shutdown: Option<ShutdownSource>,
360        schema: Option<SchemaSource>,
361        license: Option<LicenseSource>,
362        config: Option<ConfigurationSource>,
363        cli_args: Option<Opt>,
364    ) -> Result<()> {
365        let opt = cli_args.unwrap_or_else(Opt::parse);
366
367        if opt.version {
368            println!("{}", std::env!("CARGO_PKG_VERSION"));
369            return Ok(());
370        }
371
372        *crate::services::APOLLO_KEY.lock() = opt.apollo_key.clone();
373        *crate::services::APOLLO_GRAPH_REF.lock() = opt.apollo_graph_ref.clone();
374        *APOLLO_ROUTER_LISTEN_ADDRESS.lock() = opt.listen_address;
375        APOLLO_ROUTER_DEV_MODE.store(opt.dev, Ordering::Relaxed);
376        APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET
377            .store(opt.supergraph_path.is_some(), Ordering::Relaxed);
378        APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET
379            .store(opt.supergraph_urls.is_some(), Ordering::Relaxed);
380        APOLLO_ROUTER_LICENCE_IS_SET.store(opt.apollo_router_license.is_some(), Ordering::Relaxed);
381        APOLLO_ROUTER_LICENCE_PATH_IS_SET
382            .store(opt.apollo_router_license_path.is_some(), Ordering::Relaxed);
383        APOLLO_TELEMETRY_DISABLED.store(opt.anonymous_telemetry_disabled, Ordering::Relaxed);
384
385        let apollo_telemetry_initialized = if graph_os() {
386            init_telemetry(&opt.log_level)?;
387            true
388        } else {
389            // Best effort init telemetry
390            init_telemetry(&opt.log_level).is_ok()
391        };
392
393        setup_panic_handler();
394
395        let result = match opt.command.as_ref() {
396            Some(Commands::Config(ConfigSubcommandArgs {
397                command: ConfigSubcommand::Schema,
398            })) => {
399                let schema = generate_config_schema();
400                println!("{}", serde_json::to_string_pretty(&schema)?);
401                Ok(())
402            }
403            Some(Commands::Config(ConfigSubcommandArgs {
404                command: ConfigSubcommand::Validate { config_path },
405            })) => {
406                let config_string = std::fs::read_to_string(config_path)?;
407                validate_yaml_configuration(
408                    &config_string,
409                    Expansion::default()?,
410                    Mode::NoUpgrade,
411                )?
412                .validate()?;
413
414                println!("Configuration at path {config_path:?} is valid!");
415
416                Ok(())
417            }
418            Some(Commands::Config(ConfigSubcommandArgs {
419                command: ConfigSubcommand::Upgrade { config_path, diff },
420            })) => {
421                let config_string = std::fs::read_to_string(config_path)?;
422                let output = generate_upgrade(&config_string, *diff)?;
423                println!("{output}");
424                Ok(())
425            }
426            Some(Commands::Config(ConfigSubcommandArgs {
427                command: ConfigSubcommand::Experimental,
428            })) => {
429                Discussed::new().print_experimental();
430                Ok(())
431            }
432            Some(Commands::Config(ConfigSubcommandArgs {
433                command: ConfigSubcommand::Preview,
434            })) => {
435                Discussed::new().print_preview();
436                Ok(())
437            }
438            None => Self::inner_start(shutdown, schema, config, license, opt).await,
439        };
440
441        if apollo_telemetry_initialized {
442            // We should be good to shutdown OpenTelemetry now as the router should have finished everything.
443            tokio::task::spawn_blocking(move || {
444                opentelemetry::global::shutdown_tracer_provider();
445                meter_provider_internal().shutdown();
446            })
447            .await?;
448        }
449        result
450    }
451
452    async fn inner_start(
453        shutdown: Option<ShutdownSource>,
454        schema: Option<SchemaSource>,
455        config: Option<ConfigurationSource>,
456        license: Option<LicenseSource>,
457        mut opt: Opt,
458    ) -> Result<()> {
459        let current_directory = std::env::current_dir()?;
460        // Enable hot reload when dev mode is enabled
461        opt.hot_reload = opt.hot_reload || opt.dev;
462
463        let configuration = match (config, opt.config_path.as_ref()) {
464            (Some(_), Some(_)) => {
465                return Err(anyhow!(
466                    "--config and APOLLO_ROUTER_CONFIG_PATH cannot be used when a custom configuration source is in use"
467                ));
468            }
469            (Some(config), None) => config,
470            #[allow(clippy::blocks_in_conditions)]
471            _ => opt
472                .config_path
473                .as_ref()
474                .map(|path| {
475                    let path = if path.is_relative() {
476                        current_directory.join(path)
477                    } else {
478                        path.to_path_buf()
479                    };
480
481                    ConfigurationSource::File {
482                        path,
483                        watch: opt.hot_reload,
484                    }
485                })
486                .unwrap_or_default(),
487        };
488
489        let apollo_telemetry_msg = if opt.anonymous_telemetry_disabled {
490            "Anonymous usage data collection is disabled.".to_string()
491        } else {
492            "Anonymous usage data is gathered to inform Apollo product development.  See https://go.apollo.dev/o/privacy for details.".to_string()
493        };
494
495        let apollo_router_msg = format!(
496            "Apollo Router v{} // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)",
497            std::env!("CARGO_PKG_VERSION")
498        );
499
500        // Schema source will be in order of precedence:
501        // 1. Cli --supergraph
502        // 2. Env APOLLO_ROUTER_SUPERGRAPH_PATH
503        // 3. Env APOLLO_ROUTER_SUPERGRAPH_URLS
504        // 4. Env APOLLO_KEY and APOLLO_GRAPH_ARTIFACT_REFERENCE
505        // 5. Env APOLLO_KEY and APOLLO_GRAPH_REF
506        #[cfg(unix)]
507        let akp = &opt.apollo_key_path;
508        #[cfg(not(unix))]
509        let akp: &Option<PathBuf> = &None;
510
511        let schema_source = match (
512            schema,
513            &opt.supergraph_path,
514            &opt.supergraph_urls,
515            &opt.apollo_key,
516            akp,
517        ) {
518            (Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => {
519                return Err(anyhow!(
520                    "--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use"
521                ));
522            }
523            (Some(source), None, None, _, _) => source,
524            (_, Some(supergraph_path), _, _, _) => {
525                tracing::info!("{apollo_router_msg}");
526                tracing::info!("{apollo_telemetry_msg}");
527
528                let supergraph_path = if supergraph_path.is_relative() {
529                    current_directory.join(supergraph_path)
530                } else {
531                    supergraph_path.clone()
532                };
533                SchemaSource::File {
534                    path: supergraph_path,
535                    watch: opt.hot_reload,
536                }
537            }
538            (_, _, Some(supergraph_urls), _, _) => {
539                tracing::info!("{apollo_router_msg}");
540                tracing::info!("{apollo_telemetry_msg}");
541
542                if opt.hot_reload {
543                    tracing::warn!(
544                        "Schema hot reloading is disabled for --supergraph-urls / APOLLO_ROUTER_SUPERGRAPH_URLS."
545                    );
546                }
547
548                SchemaSource::URLs {
549                    urls: supergraph_urls.clone(),
550                }
551            }
552            (_, None, None, _, Some(apollo_key_path)) => {
553                let apollo_key_path = if apollo_key_path.is_relative() {
554                    current_directory.join(apollo_key_path)
555                } else {
556                    apollo_key_path.clone()
557                };
558
559                if !apollo_key_path.exists() {
560                    tracing::error!(
561                        "Apollo key at path '{}' does not exist.",
562                        apollo_key_path.to_string_lossy()
563                    );
564                    return Err(anyhow!(
565                        "Apollo key at path '{}' does not exist.",
566                        apollo_key_path.to_string_lossy()
567                    ));
568                } else {
569                    // On unix systems, Check that the executing user is the only user who may
570                    // read the key file.
571                    // Note: We could, in future, add support for Windows.
572                    #[cfg(unix)]
573                    {
574                        let meta = std::fs::metadata(apollo_key_path.clone())
575                            .map_err(|err| anyhow!("Failed to read Apollo key file: {}", err))?;
576                        let mode = meta.mode();
577                        // If our mode isn't "safe", fail...
578                        // safe == none of the "group" or "other" bits set.
579                        if mode & 0o077 != 0 {
580                            return Err(anyhow!(
581                                "Apollo key file permissions ({:#o}) are too permissive",
582                                mode & 0o000777
583                            ));
584                        }
585                        let euid = unsafe { libc::geteuid() };
586                        let owner = meta.uid();
587                        if euid != owner {
588                            return Err(anyhow!(
589                                "Apollo key file owner id ({owner}) does not match effective user id ({euid})"
590                            ));
591                        }
592                    }
593                    //The key file exists try and load it
594                    match std::fs::read_to_string(&apollo_key_path) {
595                        Ok(apollo_key) => {
596                            opt.apollo_key = Some(apollo_key.trim().to_string());
597                        }
598                        Err(err) => {
599                            return Err(anyhow!("Failed to read Apollo key file: {}", err));
600                        }
601                    };
602                    match opt.graph_artifact_reference {
603                        None => SchemaSource::Registry(opt.uplink_config()?),
604                        Some(_) => SchemaSource::OCI(opt.oci_config()?),
605                    }
606                }
607            }
608            (_, None, None, Some(_apollo_key), None) => {
609                tracing::info!("{apollo_router_msg}");
610                tracing::info!("{apollo_telemetry_msg}");
611                match opt.graph_artifact_reference {
612                    None => SchemaSource::Registry(opt.uplink_config()?),
613                    Some(_) => SchemaSource::OCI(opt.oci_config()?),
614                }
615            }
616            _ => {
617                return Err(anyhow!(
618                    r#"{apollo_router_msg}
619
620⚠️  The Apollo Router requires a composed supergraph schema at startup. ⚠️
621
622👉 DO ONE:
623
624  * Pass a local schema file with the '--supergraph' option:
625
626      $ ./router --supergraph <file_path>
627
628  * Fetch a registered schema from GraphOS by setting
629    these environment variables:
630
631      $ APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router
632
633      For details, see the Apollo docs:
634      https://www.apollographql.com/docs/federation/managed-federation/setup
635
636🔬 TESTING THINGS OUT?
637
638  1. Download an example supergraph schema with Apollo-hosted subgraphs:
639
640    $ curl -L https://supergraph.demo.starstuff.dev/ > starstuff.graphql
641
642  2. Run the Apollo Router in development mode with the supergraph schema:
643
644    $ ./router --dev --supergraph starstuff.graphql
645
646    "#
647                ));
648            }
649        };
650
651        // Order of precedence for licenses:
652        // 1. explicit path from cli
653        // 2. env APOLLO_ROUTER_LICENSE
654        // 3. uplink
655
656        let license = if let Some(license) = license {
657            license
658        } else {
659            match (
660                &opt.apollo_router_license,
661                &opt.apollo_router_license_path,
662                &opt.apollo_key,
663                &opt.apollo_graph_ref,
664            ) {
665                (_, Some(license_path), _, _) => {
666                    let license_path = if license_path.is_relative() {
667                        current_directory.join(license_path)
668                    } else {
669                        license_path.clone()
670                    };
671                    LicenseSource::File {
672                        path: license_path,
673                        watch: opt.hot_reload,
674                    }
675                }
676                (Some(_license), _, _, _) => LicenseSource::Env,
677                (_, _, Some(_apollo_key), Some(_apollo_graph_ref)) => {
678                    LicenseSource::Registry(opt.uplink_config()?)
679                }
680
681                _ => LicenseSource::default(),
682            }
683        };
684
685        // If there are custom plugins then if RUST_LOG hasn't been set and APOLLO_ROUTER_LOG contains one of the defaults.
686        let user_plugins_present = plugins().filter(|p| !p.is_apollo()).count() > 0;
687        let rust_log_set = std::env::var("RUST_LOG").is_ok();
688        let apollo_router_log = std::env::var("APOLLO_ROUTER_LOG").unwrap_or_default();
689        if user_plugins_present
690            && !rust_log_set
691            && ["trace", "debug", "warn", "error", "info"].contains(&apollo_router_log.as_str())
692        {
693            tracing::info!(
694                "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"
695            );
696        }
697
698        let uplink_config = opt.uplink_config().ok();
699        if uplink_config
700            .clone()
701            .unwrap_or_default()
702            .endpoints
703            .unwrap_or_default()
704            .url_count()
705            == 1
706        {
707            tracing::warn!(
708                "Only a single uplink endpoint is configured. We recommend specifying at least two endpoints so that a fallback exists."
709            );
710        }
711
712        let router = RouterHttpServer::builder()
713            .is_telemetry_disabled(opt.anonymous_telemetry_disabled)
714            .configuration(configuration)
715            .and_uplink(uplink_config)
716            .schema(schema_source)
717            .license(license)
718            .shutdown(shutdown.unwrap_or(ShutdownSource::CtrlC))
719            .start();
720
721        if let Err(err) = router.await {
722            tracing::error!("{}", err);
723            return Err(err.into());
724        }
725        Ok(())
726    }
727}
728
729fn graph_os() -> bool {
730    crate::services::APOLLO_KEY.lock().is_some()
731        && crate::services::APOLLO_GRAPH_REF.lock().is_some()
732}
733
734fn setup_panic_handler() {
735    // Redirect panics to the logs.
736    let backtrace_env = std::env::var("RUST_BACKTRACE");
737    let show_backtraces =
738        backtrace_env.as_deref() == Ok("1") || backtrace_env.as_deref() == Ok("full");
739    if show_backtraces {
740        tracing::warn!(
741            "RUST_BACKTRACE={} detected. This is useful for diagnostics but will have a performance impact and may leak sensitive information",
742            backtrace_env.as_ref().unwrap()
743        );
744    }
745    std::panic::set_hook(Box::new(move |e| {
746        if show_backtraces {
747            let backtrace = std::backtrace::Backtrace::capture();
748            tracing::error!("{}\n{}", e, backtrace)
749        } else {
750            tracing::error!("{}", e)
751        }
752
753        // Once we've panic'ed the behaviour of the router is non-deterministic
754        // We've logged out the panic details. Terminate with an error code
755        std::process::exit(1);
756    }));
757}
758
759#[cfg(test)]
760mod tests {
761    use crate::executable::add_log_filter;
762
763    #[test]
764    fn simplest_logging_modifications() {
765        for level in ["off", "error", "warn", "info", "debug", "trace"] {
766            assert_eq!(
767                add_log_filter(level).expect("conversion works"),
768                format!("info,apollo_router={level}")
769            );
770        }
771    }
772
773    // It's hard to have comprehensive tests for this kind of functionality,
774    // so this set is derived from the examples at:
775    // https://docs.rs/env_logger/latest/env_logger/#filtering-results
776    // which is a reasonably corpus of things to test.
777    #[test]
778    fn complex_logging_modifications() {
779        assert_eq!(add_log_filter("hello").unwrap(), "info,hello");
780        assert_eq!(add_log_filter("trace").unwrap(), "info,apollo_router=trace");
781        assert_eq!(add_log_filter("TRACE").unwrap(), "info,apollo_router=trace");
782        assert_eq!(add_log_filter("info").unwrap(), "info,apollo_router=info");
783        assert_eq!(add_log_filter("INFO").unwrap(), "info,apollo_router=info");
784        assert_eq!(add_log_filter("hello=debug").unwrap(), "info,hello=debug");
785        assert_eq!(add_log_filter("hello=DEBUG").unwrap(), "info,hello=debug");
786        assert_eq!(
787            add_log_filter("hello,std::option").unwrap(),
788            "info,hello,std::option"
789        );
790        assert_eq!(
791            add_log_filter("error,hello=warn").unwrap(),
792            "info,apollo_router=error,hello=warn"
793        );
794        assert_eq!(
795            add_log_filter("error,hello=off").unwrap(),
796            "info,apollo_router=error,hello=off"
797        );
798        assert_eq!(add_log_filter("off").unwrap(), "info,apollo_router=off");
799        assert_eq!(add_log_filter("OFF").unwrap(), "info,apollo_router=off");
800        assert_eq!(add_log_filter("hello/foo").unwrap(), "info,hello/foo");
801        assert_eq!(add_log_filter("hello/f.o").unwrap(), "info,hello/f.o");
802        assert_eq!(
803            add_log_filter("hello=debug/foo*foo").unwrap(),
804            "info,hello=debug/foo*foo"
805        );
806        assert_eq!(
807            add_log_filter("error,hello=warn/[0-9]scopes").unwrap(),
808            "info,apollo_router=error,hello=warn/[0-9]scopes"
809        );
810        // Add some hard ones
811        assert_eq!(
812            add_log_filter("hyper=debug,warn,regex=warn,h2=off").unwrap(),
813            "info,hyper=debug,apollo_router=warn,regex=warn,h2=off"
814        );
815        assert_eq!(
816            add_log_filter("hyper=debug,apollo_router=off,regex=info,h2=off").unwrap(),
817            "info,hyper=debug,apollo_router=off,regex=info,h2=off"
818        );
819        assert_eq!(
820            add_log_filter("apollo_router::plugins=debug").unwrap(),
821            "info,apollo_router::plugins=debug"
822        );
823    }
824
825    #[test]
826    fn test_validate_oci_reference_valid_cases() {
827        // Test valid OCI references with different hash values
828        let valid_hashes = vec![
829            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
830            "@sha256:ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890",
831            "@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
832            "@sha256:0000000000000000000000000000000000000000000000000000000000000000",
833            "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
834        ];
835
836        for hash in valid_hashes {
837            let result = super::Opt::validate_oci_reference(hash);
838            assert!(result.is_ok(), "Hash '{}' should be valid", hash);
839            assert_eq!(result.unwrap(), hash);
840        }
841    }
842
843    #[test]
844    fn test_validate_oci_reference_invalid_cases() {
845        let invalid_references = vec![
846            // Missing @sha256: prefix
847            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
848            // Wrong prefix
849            "@sha1:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
850            "@sha512:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
851            // Too short
852            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde",
853            // Too long
854            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1",
855            // Invalid characters
856            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeg",
857            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde!",
858            // Empty string
859            "",
860            // Just the prefix
861            "@sha256:",
862            // Hash with spaces
863            "@sha256: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
864            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ",
865            // Hash with dashes
866            "@sha256:12345678-90abcdef-12345678-90abcdef-12345678-90abcdef-12345678-90abcdef",
867            // Hash with colons
868            "@sha256:12345678:90abcdef:12345678:90abcdef:12345678:90abcdef:12345678:90abcdef",
869            // Missing hash entirely
870            "@sha256",
871            // Wrong format entirely
872            "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
873            // Extra characters at the end
874            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef:latest",
875            "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef@tag",
876        ];
877
878        for reference in invalid_references {
879            let result = super::Opt::validate_oci_reference(reference);
880            assert!(
881                result.is_err(),
882                "Reference '{}' should be invalid",
883                reference
884            );
885            let error_msg = result.unwrap_err().to_string();
886            assert!(
887                error_msg.contains("invalid graph artifact reference"),
888                "Error message should contain 'invalid graph artifact reference' for '{}'",
889                reference
890            );
891            assert!(
892                error_msg.contains(reference),
893                "Error message should contain the invalid reference '{}'",
894                reference
895            );
896        }
897    }
898}