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

server: implement metrics endpoint and test #3

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
820 changes: 814 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
]
++ lib.optionals stdenv.isDarwin [
libiconv
darwin.apple_sdk.frameworks.Security
];

# the coverage report will run the tests
Expand All @@ -86,6 +87,45 @@
};

packages = {
run-stack = pkgs.writeShellApplication {
name = "run-stack";
runtimeInputs = [
pkgs.tinyproxy
pkgs.simple-http-server
];
text =
let
proxyConfig = pkgs.writeTextFile {
name = "proxy.conf";
text = ''
ReversePath "/" "http://0.0.0.0:8001/"
ReversePath "/api/" "http://0.0.0.0:8002/api/"
ReverseOnly Yes
Port 8000
ReverseBaseURL "http://0.0.0.0:8000/"
'';
};
in
''
simple-http-server --index --port 8001 frontend &
PID_FRONTEND=$!
cargo run -- --listen-address 0.0.0.0:8002 &
PID_BACKEND=$!
tinyproxy -d -c ${proxyConfig} &
PID_PROXY=$!

cleanup() {
kill $PID_FRONTEND $PID_BACKEND $PID_PROXY
wait $PID_FRONTEND $PID_BACKEND $PID_PROXY
exit 0
}

trap cleanup SIGINT

wait $PID_FRONTEND $PID_BACKEND $PID_PROXY
'';
};

server-deps = craneLib.buildDepsOnly commonAttrs;

server-docs = craneLib.cargoDoc (
Expand Down
21 changes: 21 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css">
<script src="https://unpkg.com/[email protected]" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>
<title>Demo</title>
</head>
<body>
<div id="app" hx-get="/api/v1/items" hx-trigger="load">
</div>
<noscript>
<p lang="en">
This website requires JavaScript. Here are the
<a href="https://www.enable-javascript.com/en/">instructions how to enable JavaScript in your web browser</a>.
Or perhaps you need to make an exception in your script blocker.
</p>
</noscript>
</body>
</html>
2 changes: 2 additions & 0 deletions frontend/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import "https://unpkg.com/open-props";
@import "https://unpkg.com/open-props/normalize.min.css";
1 change: 1 addition & 0 deletions nixos/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
self.nixosModules.server
];
services.server.enable = true;
services.server.metrics.enable = true;
};
verifyServices = [ "server.service" ];
};
Expand Down
37 changes: 32 additions & 5 deletions nixos/modules/server.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ let
types
mkOption
mkIf
mkMerge
mkEnableOption
escapeShellArgs
optionals
;
cfg = config.services.server;
in
Expand Down Expand Up @@ -37,6 +37,27 @@ in
'';
};

metrics = {
enable = lib.mkEnableOption "Prometheus metrics server";

address = mkOption {
type = types.str;
default = "0.0.0.0";
example = "0.0.0.0";
description = ''
Listen address of the metrics server.
'';
};

port = mkOption {
type = types.port;
default = 8081;
description = ''
Listen port of the metrics service.
'';
};
};

logLevel = mkOption {
type = types.str;
default = "info";
Expand All @@ -50,10 +71,16 @@ in

systemd.services.server =
let
args = escapeShellArgs [
"--listen-address"
"${cfg.address}:${toString cfg.port}"
];
args = escapeShellArgs (
[
"--listen-address"
"${cfg.address}:${toString cfg.port}"
]
++ optionals cfg.metrics.enable [
"--metrics-listen-address"
"${cfg.metrics.address}:${toString cfg.metrics.port}"
]
);
in
{
description = "server";
Expand Down
5 changes: 5 additions & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ version = "0.1.0"
edition = "2021"

[dependencies]
askama = "0.12"
axum = { version = "0.7", features = ["tracing"] }
axum-extra = { version = "0.9", features = ["typed-routing"] }
clap = { version = "4", features = ["derive"] }
lazy_static = "1.5.0"
metrics = "0.23"
metrics-exporter-prometheus = "0.15"
tokio = { version = "1", features = [ "full", "tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["v4"] }

[dev-dependencies]
ureq = "2"
Expand Down
2 changes: 2 additions & 0 deletions server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
pub struct CliArgs {
#[arg(short, long, default_value_t = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), 8080, 0, 0)))]
pub listen_address: SocketAddr,
#[arg(short, long)]
pub metrics_listen_address: Option<SocketAddr>,
}
15 changes: 15 additions & 0 deletions server/src/item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use askama::Template;
use uuid::Uuid;

#[derive(Debug, Clone)]
pub struct Item {
pub id: Uuid,
pub name: String,
pub price: u128,
}

#[derive(Template)]
#[template(path = "items.html")]
pub struct ItemsTemplate<'a> {
pub items: &'a Vec<Item>,
}
108 changes: 99 additions & 9 deletions server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,115 @@
pub mod cli;
pub mod item;
pub mod routes;

use crate::cli::CliArgs;

use axum::Router;
use axum::{
extract::{MatchedPath, Request},
middleware::{self, Next},
response::IntoResponse,
routing, Router,
};
use axum_extra::routing::RouterExt;
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
use std::future;
use tokio::net::TcpListener;
use tokio::time::Instant;

static INIT: std::sync::Once = std::sync::Once::new();

pub fn init_tracing() {
INIT.call_once(|| {
tracing_subscriber::fmt().init();
});
}

/// Runs the server given the cli arguments parameters.
pub async fn run(CliArgs { listen_address }: CliArgs) -> std::io::Result<()> {
tracing_subscriber::fmt::init();
pub async fn run(
CliArgs {
listen_address,
metrics_listen_address,
}: CliArgs,
) -> std::io::Result<()> {
init_tracing();

let app = app();
let tcp_listener = TcpListener::bind(listen_address)
.await
.expect("Failed to bind to the listen address '{listen_address}'");

let tcp_listener = TcpListener::bind(listen_address).await?;
tracing::info!("Server is listening on '{listen_address}'");
axum::serve(tcp_listener, app).await

match metrics_listen_address {
None => axum::serve(tcp_listener, app).await,
Some(metrics_listen_address) => {
let metrics_app = metrics_app();
let metrics_tcp_listener = tokio::net::TcpListener::bind(metrics_listen_address)
.await
.unwrap();
tracing::info!("Metrics server is listening on {metrics_listen_address}");

// Note that this does not spawn two top-level tasks, thus this will run
// concurrently. To make this parrallel, tokio::task::spawn and then join.
tokio::try_join!(
axum::serve(tcp_listener, app),
axum::serve(metrics_tcp_listener, metrics_app)
)
.map(|_| ())
.map_err(|err| {
tracing::error!("{err}");
err
})
}
}
}

pub fn app() -> Router {
Router::new().typed_get(routes::get_items_handler)
Router::new()
.typed_get(routes::get_items_handler)
.route_layer(middleware::from_fn(track_metrics))
}

pub fn metrics_app() -> Router {
// Cannot be celled in future::ready`
let metrics_recorder = install_metrics_recorder();
Router::new().route(
"/metrics",
routing::get(move || future::ready(metrics_recorder.render())),
)
}

pub fn install_metrics_recorder() -> PrometheusHandle {
const EXPONENTIAL_SECONDS: &[f64] = &[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Full("http_requests_duration_seconds".to_string()),
EXPONENTIAL_SECONDS,
)
.unwrap()
.install_recorder()
.unwrap()
}

pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse {
let start = Instant::now();
let path = match req.extensions().get::<MatchedPath>() {
None => req.uri().path().to_owned(),
Some(matched_path) => matched_path.as_str().to_owned(),
};
let method = req.method().clone();
let response = next.run(req).await;

let latency = start.elapsed().as_secs_f64();
let status = response.status().as_u16().to_string();

let labels = [
("method", method.to_string()),
("path", path),
("status", status),
];

metrics::counter!("http_requests_total", &labels).increment(1);
metrics::histogram!("http_requests_duration_seconds", &labels).record(latency);

response
}
21 changes: 18 additions & 3 deletions server/src/routes.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
use axum::http::StatusCode;
use askama::Template;
use axum::response::Html;
use axum_extra::routing::TypedPath;
use lazy_static::lazy_static;
use std::sync::Mutex;
use tracing::instrument;

use crate::item::{Item, ItemsTemplate};

lazy_static! {
static ref ITEMS: Mutex<Vec<Item>> = Mutex::new(vec![Item {
id: uuid::Uuid::new_v4(),
name: "test".to_string(),
price: 12345
}]);
}

#[derive(TypedPath, Debug, Clone, Copy)]
#[typed_path("/api/v1/items")]
pub struct GetItemsPath;

#[instrument(level = "trace", ret)]
pub async fn get_items_handler(_: GetItemsPath) -> (StatusCode, &'static str) {
(StatusCode::OK, "Hello World!")
pub async fn get_items_handler(_: GetItemsPath) -> Html<String> {
let items = ITEMS.lock().unwrap();
let template = ItemsTemplate { items: &items };
Html(template.render().unwrap())
}
12 changes: 12 additions & 0 deletions server/templates/items.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<table>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
{% endfor %}
</table>
Loading