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
*/