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.",
        ),
    }
}