Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a monitoring feature for Core #69

Merged
merged 18 commits into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ jobs:
with:
command: check
args: --manifest-path examples/host/Cargo.toml

- name: Check monitor example
uses: actions-rs/cargo@v1
with:
command: check
args: --manifest-path examples/monitor/Cargo.toml

- name: Check plugin example
uses: actions-rs/cargo@v1
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Using State](core/state.md)
- [Static Content](core/static-content.md)
- [Using HTTPS](core/https.md)
- [Monitoring Events](core/monitoring.md)
- [Humphrey Server](server/index.md)
- [Getting Started](server/getting-started.md)
- [Configuration](server/configuration.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/core/https.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ let app: App<()> = App::new()
This starts a background thread which simply redirects HTTP requests to the corresponding HTTPS URL.

## Conclusion
In this section, we've covered how to use the TLS feature of Humphrey, and how to use it to serve HTTPS applications. If you want to learn more about Humphrey, consider exploring the [API reference](https://docs.rs/humphrey) or reading the [WebSocket guide](../websocket/index.md)
In this section, we've covered how to use the TLS feature of Humphrey, and how to use it to serve HTTPS applications. Next, we'll learn how to monitor internal events in the application.
1 change: 1 addition & 0 deletions docs/src/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ This section of the guide will cover the following topics:
2. [Handling state between requests](state.md)
3. [Integrating static and dynamic content](static-content.md)
4. [Serving applications over HTTPS](https.md)
5. [Monitoring and logging internal events](monitoring.md)

It's recommended that you have basic familiarity with Rust before reading this section, as only Humphrey-specific concepts are explained, and knowledge of the Rust language is required to understand many of them.
139 changes: 139 additions & 0 deletions docs/src/core/monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Monitoring Events
In this chapter, we'll discuss how to monitor internal events in the application. It can often be useful to log events such as requests and errors, which can be useful for debugging and for general performance analysis. To learn about this, we're going to build a simple logger which logs all events in a "Hello, world!" application to both the console, and specific events to a file.

## Setting up our Application
We'll start with an extremely simple application which simply responds with "Hello, world!" to every request. After creating a new crate and adding Humphrey as a dependency, as outlined in [Getting Started](getting-started.md), add the following code to the main file.

```rs
use humphrey::http::{Response, StatusCode};
use humphrey::App;

fn main() {
let app: App =
App::new().with_stateless_route("/*", |_| Response::new(StatusCode::OK, "Hello, world!"));

app.run("0.0.0.0:80").unwrap();
}
```

This will just return "Hello, world!" to every request.

## Logging to the Console
Monitoring events in Humphrey is done over a channel of `Event`s. An `Event` is a simple struct which contains the event's type, as well as an optional address of the client and an optional string with additional information. We'll cover this in more detail later, but for now we just need to know that it implements the `Display` trait so that we can print it.

Monitoring is configured using the `MonitorConfig` struct, and events can be subscribed to using its `with_subscription_to` function. You can subscribe to a single event or an event level (such as warning), since both types implement the `ToEventMask` trait.

Let's create a channel and supply it to the application using the `with_monitor` method, as well as subscribing to all events using the debug event level. We'll also create a thread to listen on the channel and print all events to the console.

```rs
use humphrey::http::{Response, StatusCode};
use humphrey::monitor::event::EventLevel;
use humphrey::monitor::MonitorConfig;
use humphrey::App;

use std::sync::mpsc::channel;
use std::thread::spawn;

fn main() {
let (tx, rx) = channel();

let app: App = App::new()
.with_monitor(MonitorConfig::new(tx).with_subscription_to(EventLevel::Debug))
.with_stateless_route("/*", |_| Response::new(StatusCode::OK, "Hello, world!"));

spawn(move || {
for e in rx {
println!("{}", e);
}
});

app.run("0.0.0.0:80").unwrap();
}
```

If we run this application and visit it in the browser, you'll see a lot of debug output in the console. You have successfully logged some internal events!

## Filtering Events
Events should always be filtered at the `MonitorConfig` level if possible to reduce the traffic on the channel. However, if, for example, you want to print everything to the console but only write warnings and errors to a file, you can filter events using event masks.

An event mask is simply a `u32` which is a bit mask of the events you want to subscribe to. As an example, `EventLevel::Debug` is simply `0xFFFFFFFF`, which means that all events are subscribed to. The individual event `EventType::RequestServedSuccess` is `0x00000040`. You probably don't need to know the bit mask values, but they are useful for understanding how event filtering works.

Let's move our event listening thread to a new function, and temporarily filter out all events except warnings and errors.

```rs
use std::sync::mpsc::{channel, Receiver};
use humphrey::monitor::event::{Event, EventLevel};

// --snip--

spawn(move || monitor_thread(rx));

// --snip--

fn monitor_thread(rx: Receiver<Event>) {
for e in rx {
println!("{}", e);
}
}
```

Now that our thread is in a new function, we can add the following code to filter out all events except warnings and errors.

```rs
fn monitor_thread(rx: Receiver<Event>) {
for e in rx {
if e.kind as u32 & EventLevel::Warning as u32 != 0 {
println!("{}", e);
}
}
}
```

If you run the program now, you'll probably see no output in the console, as none of the events being received are warnings or errors. If you use a tool like Netcat to send an invalid request to the application, you'll see an error message.

## Writing Events to a File
Let's add a little bit more code to the monitor thread to write all events to a file.

```rs
use std::fs::File;
use std::io::Write;

// --snip--

fn monitor_thread(rx: Receiver<Event>) {
let mut file = File::create("monitor.log").unwrap();

for e in rx {
if e.kind as u32 & EventLevel::Warning as u32 != 0 {
file.write_all(format!("{}\n", e).as_bytes()).unwrap();
}

println!("{}", e);
}
}
```

If we run the code again, we'll see that all of our events are again logged to the console, but warnings and errors are additionally logged to the file. Since our program isn't producing any errors at the moment, let's intentionally cause some by getting a thread to panic!

## Monitoring Thread Panics
**Threads should not ever panic.** However, in any application there's always a chance of bugs causing threads to panic, and this should not cause the whole program to stop working. Humphrey automatically detects thread panics in the background and quietly restarts the affected thread, while letting Rust log the panic to the console in the typical way.

If your monitor is subscribed to the `EventType::ThreadPanic` event, whether directly or through any event level, Humphrey will take over the panic logging from Rust, and will send the panic to the monitor channel instead of printing it to the standard error stream. It's important that this is logged in some way, as you should never miss a panic!

Let's cause a panic on the route "/panic" by adding another simple handler.

```rs
// --snip--

let app: App = App::new()
.with_monitor(MonitorConfig::new(tx).with_subscription_to(EventLevel::Debug))
.with_stateless_route("/panic", |_| panic!("this is a panic"))
.with_stateless_route("/*", |_| Response::new(StatusCode::OK, "Hello, world!"));

// --snip--
```

If you visit the panic route in your browser now, you won't get a response from the server as the thread has panicked, but you'll see the panic in the console and the file, as well as that the thread was restarted in the console.

## Conclusion
In conclusion, Humphrey provides a flexible way for logging internal events. If you want to learn more about Humphrey, consider exploring the [API reference](https://docs.rs/humphrey) or reading the [WebSocket guide](../websocket/index.md).
11 changes: 11 additions & 0 deletions examples/monitor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "monitor"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
humphrey = { path = "../../humphrey" }

[workspace]
32 changes: 32 additions & 0 deletions examples/monitor/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use humphrey::http::{Request, Response, StatusCode};
use humphrey::monitor::event::{Event, EventLevel};
use humphrey::monitor::MonitorConfig;
use humphrey::App;

use std::error::Error;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread::spawn;
use std::time::Duration;

fn main() -> Result<(), Box<dyn Error>> {
let (tx, rx): (Sender<Event>, Receiver<Event>) = channel();

let app: App<()> = App::new_with_config(4, ())
.with_connection_timeout(Some(Duration::from_secs(5)))
.with_monitor(MonitorConfig::new(tx).with_subscription_to(EventLevel::Debug))
.with_stateless_route("/", home);

spawn(move || {
for event in rx {
println!("{}", event);
}
});

app.run("0.0.0.0:80")?;

Ok(())
}

fn home(_: Request) -> Response {
Response::new(StatusCode::OK, "<html><body><h1>Home</h1></body></html>")
}
2 changes: 1 addition & 1 deletion humphrey-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords = ["authentication"]
categories = ["authentication"]

[dependencies]
humphrey = { version = "^0.4", path = "../humphrey" }
humphrey = { version = ">=0.4, <0.6", path = "../humphrey" }
argon2 = "0.3"
uuid = { version = "0.8", features = [ "v4" ] }
rand_core = { version = "0.6", features = ["std"] }
Expand Down
2 changes: 1 addition & 1 deletion humphrey-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords = ["http", "server", "http-server"]
categories = ["web-programming::http-server", "network-programming", "command-line-utilities"]

[dependencies]
humphrey = { version = "^0.4.4", path = "../humphrey" }
humphrey = { version = ">=0.4.4, <0.6", path = "../humphrey" }
libloading = { version = "0.7", optional = true }

[features]
Expand Down
2 changes: 1 addition & 1 deletion humphrey-ws/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ categories = ["web-programming::websocket", "network-programming"]
doctest = false

[dependencies]
humphrey = { version = "^0.4.1", path = "../humphrey" }
humphrey = { version = ">=0.4.1, <0.6", path = "../humphrey" }
2 changes: 1 addition & 1 deletion humphrey/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "humphrey"
version = "0.4.4"
version = "0.5.0"
edition = "2018"
license = "MIT"
homepage = "https://github.com/w-henderson/Humphrey"
Expand Down
Loading