1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
#![warn(missing_docs, clippy::missing_docs_in_private_items)]
//! Web server for [Merino](../merino/index.html)'s public API.
mod endpoints;
mod errors;
mod extractors;
mod middleware;
pub mod providers;
use actix_cors::Cors;
use actix_web::{
dev::Server,
get,
web::{self, Data},
App, HttpResponse, HttpServer,
};
use actix_web_location::{providers::FallbackProvider, Location};
use anyhow::Context;
use cadence::StatsdClient;
use merino_settings::Settings;
use std::net::TcpListener;
use tracing_actix_web_mozlog::MozLog;
use crate::providers::SuggestionProviderRef;
/// Run the web server
///
/// The returned server is a `Future` that must either be `.await`ed, or run it
/// as a background task using `tokio::spawn`.
///
/// Most of the details from `settings` will be respected, except for those that
/// go into building the listener (the host and port). If you want to respect the
/// settings specified in that object, you must include them in the construction
/// of `listener`.
///
/// # Context
///
/// The code initialized here emits logs and metrics, assuming global defaults
/// have been set. Logs are emitted via [`tracing`], and metrics via
/// [`cadence`].
///
/// # Errors
///
/// Returns an error if the server cannot be started on the provided listener.
///
/// # Examples
///
/// Run the server in the foreground. This will only return if there is an error
/// that causes the server to shut down. This is used to run Merino as a service,
/// such as in production.
///
/// ```no_run
/// # tokio_test::block_on(async {
/// let listener = std::net::TcpListener::bind("127.0.0.1:8080")
/// .expect("Failed to bind port");
/// let settings = merino_settings::Settings::load().await
/// .expect("Failed to load settings");
/// let metrics_client = cadence::StatsdClient::from_sink("merino", cadence::NopMetricSink);
/// let providers = merino_web::providers::SuggestionProviderRef::init(settings.clone(), metrics_client.clone())
/// .await
/// .expect("Could not create providers");
/// let server = merino_web::run(listener, metrics_client, settings, providers)
/// .expect("Failed to start server")
/// .await
/// .expect("Fatal error while running server");
/// # })
/// ```
///
/// Run the server as a background task. This will return immediately and process
/// requests. This is useful for tests.
///
/// ```no_run
/// # tokio_test::block_on(async {
/// use std::net::TcpListener;
/// use merino_settings::Settings;
///
/// let listener = TcpListener::bind("127.0.0.1:8080")
/// .expect("Failed to bind port");
/// let settings = merino_settings::Settings::load().await
/// .expect("Failed to load settings");
/// let metrics_client = cadence::StatsdClient::from_sink("merino", cadence::NopMetricSink);
/// let providers = merino_web::providers::SuggestionProviderRef::init(settings.clone(), metrics_client.clone())
/// .await
/// .expect("Could not create providers");
/// let server = merino_web::run(listener, metrics_client, settings, providers)
/// .expect("Failed to start server");
///
/// /// The server can be stopped with `join_handle::abort()`, if needed.
/// let join_handle = tokio::spawn(server);
/// # })
/// ```
pub fn run(
listener: TcpListener,
metrics_client: StatsdClient,
settings: Settings,
providers: SuggestionProviderRef,
) -> Result<Server, anyhow::Error> {
let num_workers = settings.http.workers;
let moz_log = MozLog::default();
let location_config = Data::new({
let mut config =
actix_web_location::LocationConfig::default().with_metrics(metrics_client.clone());
if let Some(ref mmdb) = settings.location.maxmind_database {
config = config.with_provider(
actix_web_location::providers::MaxMindProvider::from_path(mmdb).context(
format!(
"Could not set up maxmind location provider with database at {}",
mmdb.to_string_lossy(),
),
)?,
);
}
config.with_provider(FallbackProvider::new(Location::build()))
});
let mut server = HttpServer::new(move || {
App::new()
// App state
.app_data(Data::new((&settings).clone()))
.app_data(location_config.clone())
.app_data(Data::new(metrics_client.clone()))
.app_data(Data::new(providers.clone()))
// Middlewares
.wrap(moz_log.clone())
.wrap(middleware::Metrics)
.wrap(middleware::Sentry)
.wrap(Cors::permissive())
// The core functionality of Merino
.service(web::scope("api/v1/suggest").configure(endpoints::suggest::configure))
.service(web::scope("api/v1/providers").configure(endpoints::providers::configure))
// Add some debugging views
.service(web::scope("debug").configure(endpoints::debug::configure))
.service(root_info)
// Add the behavior necessary to satisfy Dockerflow.
.service(web::scope("").configure(endpoints::dockerflow::configure))
})
.listen(listener)?;
if let Some(n) = num_workers {
server = server.workers(n);
}
let server = server.run();
Ok(server)
}
/// The root view, to provide information about what this service is.
///
/// This is intended to be seen by people trying to investigate what this service
/// is. It should redirect to documentation, if it is available, or provide a
/// short message otherwise.
#[get("/")]
pub fn root_info(settings: Data<Settings>) -> HttpResponse {
match &settings.public_documentation {
Some(redirect_url) => HttpResponse::Found()
.insert_header(("location", redirect_url.to_string()))
.finish(),
None => HttpResponse::Ok().content_type("text/plain").body(
"Merino is a Mozilla service providing information to the Firefox Suggest feature.",
),
}
}