179 lines
6.5 KiB
Markdown
179 lines
6.5 KiB
Markdown
|
+++
|
||
|
title = "Telemetry: Logging and Analytics"
|
||
|
date = 2023-03-20T17:38:12Z
|
||
|
weight = 6
|
||
|
+++
|
||
|
|
||
|
## Chapter 4: Telemetry
|
||
|
|
||
|
In Chapter 4, Palmieri focuses on logging and telemetry. Axum is very different
|
||
|
from Actix, and my first foray into trying to understand it led me to
|
||
|
[Tower](https://docs.rs/tower/latest/tower/), the Rust community's de-facto
|
||
|
standards for modular networking development and design.
|
||
|
|
||
|
I completely short-circuited much of what the book recommended and, instead,
|
||
|
just went with the most basic implementation possible. I added the tracing
|
||
|
libraries as recommended by the Axum developers, and then implemented the first
|
||
|
level of tracing as recommended by Tower:
|
||
|
|
||
|
``` sh
|
||
|
$ cargo add --features tower_http/trace,tracing tower tower_http tracing tracing_subscriber
|
||
|
```
|
||
|
|
||
|
And then I updated the app startup code to include it:
|
||
|
|
||
|
``` rust
|
||
|
pub async fn app(configuration: &Settings) -> Router {
|
||
|
tracing_subscriber::fmt::init();
|
||
|
|
||
|
let pool = PgPoolOptions::new()
|
||
|
.max_connections(50)
|
||
|
.connect(&configuration.database.url())
|
||
|
.await
|
||
|
.expect("could not connect to database_url");
|
||
|
|
||
|
routes().layer(Extension(pool)).layer(TraceLayer::new_for_http())
|
||
|
}
|
||
|
```
|
||
|
|
||
|
That is literally all that was needed. And the output is:
|
||
|
|
||
|
``` plaintext
|
||
|
2023-03-25T16:49:06.385563Z DEBUG request{method=GET uri=/ version=HTTP/1.1}:
|
||
|
tower_http::trace::on_request: started processing request
|
||
|
2023-03-25T16:49:06.386270Z DEBUG request{method=GET uri=/ version=HTTP/1.1}:
|
||
|
tower_http::trace::on_response: finished processing request latency=0 ms
|
||
|
status=200
|
||
|
```
|
||
|
|
||
|
That's not great logging, but it's a start. As I understand it,
|
||
|
`tracing_subscriber::fmt::init()` initializes the formatter, but I'm confused as
|
||
|
to where this is saved or stored, since it seems to be... nowhere. The deeper
|
||
|
Rust gets, the wilder it seems.
|
||
|
|
||
|
What I did manage was to create, [as recommended by Chris
|
||
|
Allen](https://bitemyapp.com/blog/notes-on-zero2prod-rust/), a very simple Layer
|
||
|
that shoves a new object into the collection of data being passed around by the
|
||
|
request. That object contains a unique UUID for the session being processed.
|
||
|
Since Tokio is a multi-threaded system, having a UUID allows us to trace each
|
||
|
individual request from beginning to end... provided I've hooked up by handlers
|
||
|
just right.
|
||
|
|
||
|
I learned most of this by reading the [Axum Session source
|
||
|
code](https://docs.rs/axum-sessions/latest/src/axum_sessions/session.rs.html),
|
||
|
which implements something much more complex. Since we're at a deeper level of
|
||
|
the service handling I need a function takes a Request and returns a Response,
|
||
|
and in the middle inserts a SessionId into the Request passed in; by giving the
|
||
|
type a name any handlers can now find and use that SessionId:
|
||
|
|
||
|
``` rust
|
||
|
/// In file `session_id.rs`
|
||
|
#[derive(Clone)]
|
||
|
pub struct SessionId(pub Uuid);
|
||
|
|
||
|
pub async fn session_id<B>(mut req: Request<B>, next: Next<B>)
|
||
|
-> Result<Response, StatusCode> {
|
||
|
req.extensions_mut().insert(SessionId(Uuid::new_v4()));
|
||
|
Ok(next.run(req).await)
|
||
|
}
|
||
|
```
|
||
|
|
||
|
With that, I now need to add it to the layers initialized with the app object:
|
||
|
|
||
|
```rust
|
||
|
/// In lib.rs:pub async fn app()`:
|
||
|
routes()
|
||
|
.layer(Extension(pool))
|
||
|
.layer(TraceLayer::new_for_http())
|
||
|
.layer(middleware::from_fn(session_id::session_id))
|
||
|
```
|
||
|
|
||
|
And with that, the SessionId is available. Since it's the outermost layer, it
|
||
|
can now be used by anything deeper in. Let's add it to the `subscribe`
|
||
|
function:
|
||
|
|
||
|
``` rust
|
||
|
/// In routes/subscribe.rs/subscribe()
|
||
|
pub(crate) async fn subscribe(
|
||
|
Extension(session): Extension<SessionId>,
|
||
|
Extension(pool): Extension<PgPool>,
|
||
|
payload: Option<Form<NewSubscription>>,
|
||
|
) -> Result<(StatusCode, ()), ZTPError> {
|
||
|
if let Some(payload) = payload {
|
||
|
// Multi-line strings in Rust. Ugly. Would have preferred a macro.
|
||
|
let sql = r#"INSERT INTO subscriptions
|
||
|
(id, email, name, subscribed_at)
|
||
|
VALUES ($1, $2, $3, $4);"#.to_string();
|
||
|
let subscription: Subscription = (&(payload.0)).into();
|
||
|
|
||
|
tracing::info!(
|
||
|
"request_id {} - Adding '{}' as a new subscriber.",
|
||
|
session.0.to_string(),
|
||
|
subscription.name
|
||
|
);
|
||
|
// ...
|
||
|
```
|
||
|
|
||
|
And with that, every Request now has a strong ID associated with it:
|
||
|
|
||
|
``` plaintext
|
||
|
2023-03-26T22:19:23.305421Z INFO ztp::routes::subscribe:
|
||
|
request_id d0f4a6e7-de0d-48bc-902b-713901c1d63b -
|
||
|
Adding 'Elf M. Sternberg' as a new subscriber.
|
||
|
```
|
||
|
|
||
|
That's a very noisy trace; I'd like to start knocking it down to something more
|
||
|
like a responsible log, or give me permission to format it the way I like. I'm
|
||
|
also getting incredibly noisy messages from the `sqlx::query` call, including
|
||
|
the text of the SQL template (the `let sql = ...` line above), which I really
|
||
|
don't need every time someone makes a request, and is horribly formatted for
|
||
|
principled analytics.
|
||
|
|
||
|
Configuring it to return JSON turned out to be easy, although my first pass
|
||
|
puzzled me. I had to turn `json` formatting on as a feature:
|
||
|
|
||
|
``` sh
|
||
|
$ cargo add --features=json tracing_subscriber
|
||
|
```
|
||
|
|
||
|
And then it was possible to configure the format:
|
||
|
|
||
|
``` rust
|
||
|
// in lib.rs:app()
|
||
|
// ...
|
||
|
let format = tracing_subscriber::fmt::format()
|
||
|
.with_level(false) // don't include levels in formatted output
|
||
|
.with_thread_names(true)
|
||
|
.json(); // include the name of the current thread
|
||
|
|
||
|
tracing_subscriber::fmt().event_format(format).init();
|
||
|
// ...
|
||
|
```
|
||
|
|
||
|
``` json
|
||
|
{
|
||
|
"timestamp":"2023-03-26T22:53:13.091366Z",
|
||
|
"fields": {
|
||
|
"message":"request_id 479014e2-5f13-4e12-8401-34d8f8bf1a18 - "
|
||
|
"Adding 'Elf M. Sternberg' as a new subscriber."},
|
||
|
"target":"ztp::routes::subscribe",
|
||
|
"threadName":"tokio-runtime-worker"
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Conclusion
|
||
|
|
||
|
This pretty much concludes my week-long foray into Palmieri's book; I'm not
|
||
|
going to worry too much about the deployment stuff, since that's part of my
|
||
|
daytime job and I'm not interesting in going over it again.
|
||
|
|
||
|
Overall, this was an excellent book for teaching me many of the basics, and
|
||
|
provides a really good introduction into the way application servers can be
|
||
|
written in Rust. I disagree with the premise that "the language doesn't mean
|
||
|
anything to the outcome," as I've heard some people say, nor do I think using
|
||
|
Rust is some kind of badge of honor. Instead, I think it's a mark of a
|
||
|
responsible developer, one who can produce code that works well the first time,
|
||
|
and with some hard thinking about how types work (and some heavy-duty exposure
|
||
|
to Haskell), Rust development can be your first thought, not your "I need
|
||
|
speed!" thought, when developing HTTP-based application servers.
|