Skip to content

Commit

Permalink
what a query
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Oct 31, 2024
1 parent ebabbe8 commit 4b5aeef
Show file tree
Hide file tree
Showing 30 changed files with 426 additions and 149 deletions.
19 changes: 11 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ members = [
"rwf-macros",
"rwf-tests",
"examples/django",
"examples/request-tracking", "examples/engine", "examples/admin",
"examples/request-tracking",
"examples/engine",
"rwf-admin",
]
7 changes: 0 additions & 7 deletions examples/admin/static/js/jobs_controller.js

This file was deleted.

2 changes: 1 addition & 1 deletion examples/scheduled-jobs/rwf.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[general]

[database]
name = "rwf_bg_jobs"
name = "rwf_admin"
4 changes: 3 additions & 1 deletion examples/scheduled-jobs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ use rwf::job::{Error as JobError, Job, Worker};
use rwf::prelude::*;

use serde::{Deserialize, Serialize};
use tokio::time::sleep;

#[derive(Clone, Serialize, Deserialize, Default)]
struct MyJob;

#[rwf::async_trait]
impl Job for MyJob {
async fn execute(&self, _args: serde_json::Value) -> Result<(), JobError> {
sleep(std::time::Duration::from_secs(1)).await;
Ok(())
}
}
Expand All @@ -27,7 +29,7 @@ async fn main() -> Result<(), Error> {
.start()
.await?;

Server::new(vec![]).launch("0.0.0.0:8000").await?;
sleep(std::time::Duration::MAX).await;

Ok(())
}
7 changes: 5 additions & 2 deletions examples/admin/Cargo.toml → rwf-admin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
[package]
name = "admin"
name = "rwf-admin"
version = "0.1.0"
edition = "2021"

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

[dependencies]
rwf = { path = "../../rwf" }
rwf = { path = "../rwf" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
time = { version = "0.3", features = ["serde"] }
File renamed without changes.
151 changes: 151 additions & 0 deletions rwf-admin/src/controllers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use rwf::job::JobModel;
use rwf::prelude::*;
use rwf::serde::Serialize;

#[derive(Default)]
pub struct Index;

#[async_trait]
impl Controller for Index {
async fn handle(&self, _request: &Request) -> Result<Response, Error> {
Ok(Response::new().redirect("jobs"))
}
}

#[derive(Default)]
pub struct Jobs;

#[derive(macros::Context)]
struct JobsContext {
queued: i64,
running: i64,
errors: i64,
latency: i64,
jobs: Vec<JobModel>,
}

impl JobsContext {
pub async fn load() -> Result<Self, Error> {
let mut conn = Pool::connection().await?;
let queued = JobModel::queued().count(&mut conn).await?;
let errors = JobModel::errors().count(&mut conn).await?;
let running = JobModel::running().count(&mut conn).await?;

let jobs = JobModel::all()
.order(("id", "DESC"))
.limit(25)
.fetch_all(&mut conn)
.await?;

let latency = JobModel::queued()
.order("created_at")
.take_one()
.fetch_optional(&mut conn)
.await?;

let latency = if let Some(latency) = latency {
(OffsetDateTime::now_utc() - latency.created_at).whole_seconds()
} else {
Duration::seconds(0).whole_seconds()
};

Ok(Self {
queued,
errors,
running,
jobs,
latency,
})
}
}

#[async_trait]
impl Controller for Jobs {
async fn handle(&self, _request: &Request) -> Result<Response, Error> {
let template = Template::load("templates/rwf_admin/jobs.html")?;
Ok(Response::new().html(template.render(JobsContext::load().await?)?))
}
}

#[derive(Clone, macros::Model, Serialize)]
struct RequestByCode {
count: i64,
code: String,
#[serde(with = "time::serde::rfc2822")]
created_at: OffsetDateTime,
}

impl RequestByCode {
fn count(minutes: i64) -> Scope<Self> {
Self::find_by_sql(
"WITH timestamps AS (
SELECT date_trunc('minute', now() - (n || ' minute')::interval) AS created_at FROM generate_series(0, $1::bigint) n
)
SELECT
'ok' AS code,
COALESCE(e2.count, 0) AS count,
timestamps.created_at AS created_at
FROM timestamps
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS count,
DATE_TRUNC('minute', created_at) AS created_at
FROM rwf_requests
WHERE
created_at BETWEEN timestamps.created_at AND timestamps.created_at + INTERVAL '1 minute'
AND code BETWEEN 100 AND 299
GROUP BY 2
) e2 ON true
UNION ALL
SELECT
'warn' AS code,
COALESCE(e2.count, 0) AS count,
timestamps.created_at AS created_at
FROM timestamps
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS count,
DATE_TRUNC('minute', created_at) AS created_at
FROM rwf_requests
WHERE
created_at BETWEEN timestamps.created_at AND timestamps.created_at + INTERVAL '1 minute'
AND code BETWEEN 300 AND 499
GROUP BY 2
) e2 ON true
UNION ALL
SELECT
'error' AS code,
COALESCE(e2.count, 0) AS coount,
timestamps.created_at AS created_at
FROM timestamps
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS count,
DATE_TRUNC('minute', created_at) AS created_at
FROM rwf_requests
WHERE
created_at BETWEEN timestamps.created_at AND timestamps.created_at + INTERVAL '1 minute'
AND code BETWEEN 500 AND 599
GROUP BY 2
) e2 ON true
ORDER BY 3;",
&[minutes.to_value()],
)
}
}

#[derive(Default)]
pub struct Requests;

#[async_trait]
impl Controller for Requests {
async fn handle(&self, _request: &Request) -> Result<Response, Error> {
let requests = {
let mut conn = Pool::connection().await?;
RequestByCode::count(60).fetch_all(&mut conn).await?
};
let requests = serde_json::to_string(&requests)?;

render!("templates/rwf_admin/requests.html", "requests" => requests)
}
}
13 changes: 13 additions & 0 deletions rwf-admin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use rwf::controller::Engine;
use rwf::prelude::*;

mod controllers;
use controllers::*;

pub fn engine() -> Engine {
Engine::new(vec![
route!("/" => Index),
route!("/jobs" => Jobs),
route!("/requests" => Requests),
])
}
6 changes: 3 additions & 3 deletions examples/admin/src/main.rs → rwf-admin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use rwf::admin;
use rwf::controller::BasicAuth;
use rwf::{
controller::TurboStream,
controller::{StaticFiles, TurboStream},
http::{self, Server},
prelude::*,
};
Expand All @@ -17,14 +16,15 @@ async fn main() -> Result<(), http::Error> {

// Basic auth is just an example, it's not secure. I would recommend using SessionAuth
// and checking that the user is an admin using an internal check.
let admin = admin::engine().auth(AuthHandler::new(BasicAuth {
let admin = rwf_admin::engine().auth(AuthHandler::new(BasicAuth {
user: "admin".to_string(),
password: "admin".to_string(),
}));

Server::new(vec![
engine!("/admin" => admin),
route!("/turbo-stream" => TurboStream),
StaticFiles::serve("static")?,
])
.launch("0.0.0.0:8000")
.await
Expand Down
62 changes: 62 additions & 0 deletions rwf-admin/static/js/requests_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Controller } from "hotwired/stimulus";

export default class extends Controller {
static targets = ["requestsOk", "chart"];

connect() {
const data = JSON.parse(this.requestsOkTarget.innerHTML);
const labels = Array.from(
new Set(
data.map((item) => new Date(item.created_at).toLocaleTimeString()),
),
);
const ok = data
.filter((item) => item.code === "ok")
.map((item) => item.count);
const warn = data
.filter((item) => item.code === "warn")
.map((item) => item.count);
const error = data
.filter((item) => item.code === "error")
.map((item) => item.count);

const options = {
scales: {
x: {
ticks: {
callback: (t, i) => (i % 10 === 0 ? labels[i] : null),
},
stacked: true,
},
y: {
stacked: true,
},
},
};

const chartData = {
labels,
datasets: [
{
label: "100-299",
data: ok,
},
{
label: "500-599",
data: error,
// backgroundColor: "red",
},
{
label: "300-499",
data: warn,
},
],
};

new Chart(this.chartTarget, {
type: "bar",
data: chartData,
options,
});
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"
></script>

<script async>
import { Application } from "hotwired/stimulus";
import JobsController from "/static/js/jobs_controller.js";

const application = Application.start();
application.register("jobs", JobsController);
<script type="module" async>
import { Application } from 'hotwired/stimulus'
const application = Application.start();
import Requests from '/static/js/requests_controller.js'
application.register('requests', Requests)
</script>

</head>
<body class="grey darken-4 grey-text text-lighten-4">
<%- rwf_turbo_stream("/turbo-stream") %>
File renamed without changes.
Loading

0 comments on commit 4b5aeef

Please sign in to comment.