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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
/*!
A tutorial

# The task to tackle

Let's go, step by step, over how an application that uses `spirit` might be built and what
everything the library can offer.

Imagine you work for a company and it is business critical to have a Hello World service. It would
listen on an HTTP endpoint and greet anyone who comes. One couldn't ask for an easier task, as
that's basically the [hyper's example](https://hyper.rs/).

But then, you show it to your boss. The boss is happy with the functionality, overall, but starts
asking pointed questions about how to configure the port it listens on, or if it can listen on
multiple at once. And where the logs go, similarly about metrics, etc, etc. Overall, the actual
functionality is fine, but there's a lot of absolutely boring boilerplate to write.

# The initial conversion, introducing spirit

That's what `spirit` is about, to cut down on it a little bit. So, let's start with first
converting the application to `spirit`, without actually using anything from it.

```rust,no_run
use std::convert::Infallible;
use std::net::SocketAddr;

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use serde::Deserialize;
use spirit::prelude::*;
use spirit::Spirit;
use structopt::StructOpt;

// This'll hold our configuration. It's empty for now.
#[derive(Clone, Debug, Default, Deserialize)]
struct Cfg {

}

// Here we will have command line options. We will also add them later on. The doc string is used
// as part of the help text of the application.
/// The hello world service.
///
/// Greets people over HTTP.
#[derive(Clone, Debug, StructOpt)]
// We can use all the structopts tricks here.
#[structopt(
    version = "1.0.0-example",
    author,
)]
struct Opts {

}

async fn handle(_: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new("Hello, World!".into()))
}

#[tokio::main]
async fn main() {
    // This'll load our configuration and options. But we didn't add any yet, so we'll just leave
    // it this way.
    let _spirit = Spirit::<Opts, Cfg>::new()
        // The false asks not to have a background thread, we'll change that later.
        .build(false)
        // Don't worry, we'll deal with this unwrap later too.
        .unwrap();

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}
```

This doesn't do much. What we have added is configuration line parsing. It'll be able to accept
paths to config files (yes, possibly multiple, or directories, that are scanned for the
configuration files). The configuration is then loaded ‒ from the files, and from overrides from
the command line options. We could instruct it take some environment variables or to embed a
default configuration „file“ into the program directly. And then we could access the command line
options and the configuration ‒ though the structures for them are empty for now.

We can, of course, add whatever configuration options we like, as long as they can be deserialized
using [`serde`](https://serde.rs). We could then either read the configuration from the `spirit`
object, or hook into the changes with for example
[`on_config`][crate::extension::Extensible::on_config]. This is what can be used for options
specific for our application. But we are actually worried about the parts that belong to every
service, not just our own, so we are going to use few more bits of `spirit` for that.

# Configuring logging

Let's add logging into the application first. There are two parts. One is instrumenting our code
with all the logging macros from the [`log`] crate, link `warn` and `debug`. There's nothing
unusual here.

The other part is the logger ‒ something that formats and sends the log *somewhere*. The
[`spirit-log`] crate can create the logger from configuration and command line options. So we go to
the crate's documentation and get inspired by the example there. We add the configuration options
([`fragment`][crate::fragment::Fragment]) to our own configuration structure and install the
[`pipeline`][crate::fragment::pipeline::Pipeline] that installs the logger and updates it on
configuration reloading (it'll even reopen log files on `SIGHUP`).

```rust,no_run
use std::convert::Infallible;
use std::net::SocketAddr;

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use log::debug;
use serde::Deserialize;
use spirit::prelude::*;
use spirit::{Pipeline, Spirit};
use spirit_log::{Cfg as Logging, CfgAndOpts as LogBoth, Opts as LogOpts};
use structopt::StructOpt;

#[derive(Clone, Debug, Default, Deserialize)]
struct Cfg {
    /// The logging.
    ///
    /// This allows multiple logging destinations in parallel, configuring the format, timestamp
    /// format, destination.
    #[serde(default, skip_serializing_if = "Logging::is_empty")]
    logging: Logging,
}

/// The hello world service.
///
/// Greets people over HTTP.
#[derive(Clone, Debug, StructOpt)]
#[structopt(
    version = "1.0.0-example",
    author,
)]
struct Opts {
    // Adds the `--log` and `--log-module` options.
    #[structopt(flatten)]
    logging: LogOpts,
}

async fn handle(req: Request<Body>) -> Result<Response<Body>, Infallible> {
    debug!("Handling request {:?}", req);
    Ok(Response::new("Hello, World!".into()))
}

#[tokio::main]
async fn main() {
    let _spirit = Spirit::<Opts, Cfg>::new()
        // This is the new, interesting part. It takes both the logging configuration and command
        // line options, puts them together and tells system put it in place.
        //
        // Note that it would allow us to augment the logger on the way through the pipeline (for
        // example adding a hardcoded logger to the set provided by the configuration ‒ it can be
        // used for example to integrate with sentry, if you have that need).
        .with(
            Pipeline::new("logging").extract(|opts: &Opts, cfg: &Cfg| LogBoth {
                cfg: cfg.logging.clone(),
                opts: opts.logging.clone(),
            }),
        )
        .build(false)
        .unwrap();

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}
```

So we can now configure the log destinations in a config file (and we can set multiple ones, with
different logging levels, at once, filtering of log levels based on the log target, and some more).
Once we turn on the background thread, we'll gain ability to change logging at runtime. It comes
handy, but having to write it every time into each new application is tedious ‒ now we can reuse it
every time.

Similarly, we can pull in support for configuring metrics, a HTTP client, proper daemonization
(though it is no longer as fashionable for services to go into background on their own and some
 kind of init system thing usually handles that), etc. Hopefully, as time goes, there'll be even
more.

All these crates provide their own bits of configuration and an example that can be used as the
basic „canonical“ way to use them and tweak it to get exactly the needed support.

# Improving the UX around configuration

So we have pulled in a whole bunch of configuration fragments and we are not stopping here, we are
going to configure our own things too in a moment. And we can compose the final configuration from
multiple files, environment variables, etc. That's great, but it also means we can easily get lost
in the sheer amount of things that can be configured and knowing what configuration we run with is
also a bit of a challenge.

This is where [`spirit-cfg-helpers`] come into play. This allows us, with a bit of work, to add few
new command line switches. The `--help-config` flag is similar to help ‒ it prints the whole tree
of what *can* be configured, each option with a little desciption (yes, we'll talk about where that
comes from). The `--dump-config` does the start up of the application up to the point when the
configuration is composed from all the parts, parsed and then it just prints the whole
configuration as it *would have* been used and exits.

Apart from having to throw the right field into our configuration option structure and plugging it
in, like with the other crates, we need make sure our configuration structure implements two
additional traits.

The [`Serialize`][serde::Serialize] allows us to take the parsed configuration structure and dump
it. Usually, it can be simply derived (and it pairs with the [`Deserialize`][serde::Deserialize]
use to parse the configuration in the first place).

If you're feeling perfectionist, you can optimize the behaviour of the parsing and dumping for
better UX. For example, annotating vectors of things with `#[serde(default, skip_serializing_if =
"Vec::is_empty")]` makes the vector disappear if it has no elements. Similar with [`Option`]. There
are few utilities around, like [`deserialize_duration`][crate::utils::deserialize_duration] (so you
can specify durations in form of `3days 5hours`). There's also the [`Hidden`][crate::utils::Hidden]
to hide passwords from the dump and logs ‒ if you have a field `password: Hidden<Stryng>`, it'll be
printed to both as `***` instead the actual password.

The [`StructDoc`][structdoc::StructDoc] provides the help and the structure of the configuration
file. It is derived in a similar way to the [`serde`] derives. The help is taken from the doc
comments. If you use a type that is not known to it, you can annotate it with `#[structdoc(leaf =
"name-of-type")]` and the type in the help will be taken from there.

The support for `structdoc` is optional in the spirit extension crates, under the `cfg-help`
feature, but turned on by default.  If you disable default features, you may need to turn the
`structdoc` support on explicitly in `Cargo.toml`.

```toml
...
spirit-log = { version = "0.4", default-features = false, features = ["cfg-help"] }
```

So let's extend our example:

```rust,no_run
use std::convert::Infallible;
use std::net::SocketAddr;

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use log::debug;
use serde::{Deserialize, Serialize};
use spirit::prelude::*;
use spirit::{Pipeline, Spirit};
use spirit_log::{Cfg as Logging, CfgAndOpts as LogBoth, Opts as LogOpts};
use structdoc::StructDoc;
use structopt::StructOpt;

// We have just added the two new derives in here
#[derive(Clone, Debug, Default, Deserialize, Serialize, StructDoc)]
struct Cfg {
    /// The logging.
    ///
    /// This allows multiple logging destinations in parallel, configuring the format, timestamp
    /// format, destination.
    #[serde(default, skip_serializing_if = "Logging::is_empty")]
    logging: Logging,
}

/// The hello world service.
///
/// Greets people over HTTP.
#[derive(Clone, Debug, StructOpt)]
// We can use all the structopts tricks here.
#[structopt(
    version = "1.0.0-example",
    author,
)]
struct Opts {
    // Adds the `--log` and `--log-module` options.
    #[structopt(flatten)]
    logging: LogOpts,
}

async fn handle(req: Request<Body>) -> Result<Response<Body>, Infallible> {
    debug!("Handling request {:?}", req);
    Ok(Response::new("Hello, World!".into()))
}

#[tokio::main]
async fn main() {
    let _spirit = Spirit::<Opts, Cfg>::new()
        // This is the new, interesting part. It takes both the logging configuration and command
        // line options, puts them together and tells system put it in place.
        //
        // Note that it would allow us to augment the logger on the way through the pipeline (for
        // example adding a hardcoded logger to the set provided by the configuration ‒ it can be
        // used for example to integrate with sentry, if you have that need).
        .with(
            Pipeline::new("logging").extract(|opts: &Opts, cfg: &Cfg| LogBoth {
                cfg: cfg.logging.clone(),
                opts: opts.logging.clone(),
            }),
        )
        .build(false)
        .unwrap();

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}
```

# Configuring the web server and managing the whole lifetime

Until now, we just created the `spirit` object at the beginning, let it initialize stuff, but then
forgot about it. That's not taking the full advantage of it. What the thing can do for us is also
to reload configuration in the background, terminate when asked for, run shutdown tasks, and handle
errors during startup.

But for that we need to integrate the HTTP server into `spirit`, so it can be manipulated by it.
Spirit provides configuration fragments both for the [`tokio`] runtime (configuring number of
threads, for example ‒ up until now we have let it decide for us) and for HTTP servers. Let's pull
them in.

Then we move from using `build` to using `run` to execute the actual „body“ of the application.

```rust,no_run
use hyper::{Body, Request, Response};
use log::debug;
use serde::{Deserialize, Serialize};
use spirit::prelude::*;
use spirit::{Pipeline, Spirit};
use spirit_log::{Cfg as Logging, CfgAndOpts as LogBoth, Opts as LogOpts};
use spirit_hyper::{server_from_handler, BuildServer, HttpServer};
use spirit_tokio::runtime::Config as TokioCfg;
use spirit_tokio::Tokio;
use structdoc::StructDoc;
use structopt::StructOpt;

// We have just added the two new derives in here
#[derive(Clone, Debug, Default, Deserialize, Serialize, StructDoc)]
struct Cfg {
    /// The logging.
    ///
    /// This allows multiple logging destinations in parallel, configuring the format, timestamp
    /// format, destination.
    #[serde(default, skip_serializing_if = "Logging::is_empty")]
    logging: Logging,

    // Some more configuration things go in here.
    /// The work threadpool.
    ///
    /// This is for performance tuning.
    threadpool: TokioCfg,

    /// Where to listen on for incoming requests.
    // Makes the array "disappear" if empty. Omit to always force at least an explicit empty array.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    listen: Vec<HttpServer>,
}

impl Cfg {
    fn listen(&self) -> &Vec<HttpServer> {
        &self.listen
    }
}

/// The hello world service.
///
/// Greets people over HTTP.
#[derive(Clone, Debug, StructOpt)]
// We can use all the structopts tricks here.
#[structopt(
    version = "1.0.0-example",
    author,
)]
struct Opts {
    #[structopt(flatten)]
    logging: LogOpts,
}

async fn handle(req: Request<Body>) -> Response<Body> {
    debug!("Handling request {:?}", req);
    Response::new("Hello, World!".into())
}

// Note: The main is no longer async tokio::main thing, it is the ordinary blocking main.
fn main() {
    Spirit::<Opts, Cfg>::new()
        .with(
            Pipeline::new("logging").extract(|opts: &Opts, cfg: &Cfg| LogBoth {
                cfg: cfg.logging.clone(),
                opts: opts.logging.clone(),
            }),
        )
        // Right, this is not a pipeline. This serves a double purpose. First, it integrates the
        // tokio runtime into the spirit, which must be started *before* run (it sometimes can be
        // started implicitly as part of a pipeline that uses tokio, but the only one we have here
        // is plugged in inside the run).
        //
        // The other purpose is to actually configure how many threads run, etc.
        //
        // Rust gets confused if you don't provide at least `cfg: &_` as the type.
        .with(Tokio::from_cfg(|cfg: &Cfg| cfg.threadpool.clone()))
        // The run will first construct the spirit object, then run the body. If there's any error
        // both during the setup (before run) or inside run, the error is logged (to the configured
        // logging location, if that is already set up, or to stderr if it is very early).
        .run(|spirit| {
            // Here we could do some more loading, starting up, etc.
            // We also can access the spirit and read configuration from it.
            //
            // But actually, we just plug another pipeline in, one that sets up the http server(s).
            // We could do that before run too, but this way all the theoretical loading happens
            // before we start listening and we can also use that and pass it to the created
            // server.

            spirit.with(
                Pipeline::new("listen")
                    // Yes, we are passing the whole vector to spirit, but provide instruction how
                    // to create one server. Spirit figures on its own how to start multiple from
                    // that, which ones to add or remove on a change and all that.

                    // FIXME: Does anyone know why we need an actual function in here and inline
                    // closure is just not enough? If we just inline it here, rustc is not able to
                    // build the whole pipeline and one of the million trains don't align right,
                    // though it is confused enough not to even hint at which one. A rustc bug?
                    .extract_cfg(Cfg::listen)
                    .transform(BuildServer(server_from_handler(handle)))
            )?;

            Ok(())

            // Now, the `run` will terminate. But the spirit will wait with shutdown until the
            // futures ‒ in case the servers we started ‒ finish running.
        });
}
```

This adds a bit of copy-paste style code from docs, but we gained the ability to configure multiple
HTTP servers, including fine details like how many concurrent connections are we willing to have
open at each time, or if we want to support HTTP2. As a bonus, we get the ability to actually
reconfigure what ports (and addresses) we listen on at runtime.

There are few more tricks. It is possible to also listen on unix domain sockets, or even create a
hybrid configuration where some instances listen on IPv4, some on IPv6 and some on unix domain
sockets. Furthermore, the server configuration fragment can be parametrized by additional type
parameter. The library doesn't touch it, but it is possible ‒ if you don't use the
`server_from_handler`, but build the server manually ‒ to access that field and customize each
separate instance of the listening server.

# The complete thing

The repository contains
[an example](https://github.com/vorner/spirit/blob/master/examples/hws-complete.rs) that is an
extended version of the above exercise. You'll further find:

* Some more logging, including of the content of newly loaded configuration.
* Embedding of base configuration inside the program.
* Looking for configuration inside the environment.
* Parametrizing the behaviour of the server by both global (whole application) and local (one
  server instance) configuration.
* Support for the unix domain sockets.
* Daemonization.
* Manual building of the server, which allows for returning errors, for passing context and other a
  bit more advanced things.

Hopefully, this shows some of the possibilities of what spirit is capable of. Further chapters do
into more details about specific topics.

[`tokio`]: https://tokio.rs
[`spirit-log`]: https://lib.rs/crates/spirit-log
[`spirit-cfg-helpers`]: https://lib.rs/crates/spirit-cfg-helpers
*/