Compare commits
1 Commits
refactor-i
...
7110a9e8f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
7110a9e8f6
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
*.db
|
||||||
|
|||||||
1035
Cargo.lock
generated
1035
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,8 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["snitch", "snitch_srv"]
|
members = [
|
||||||
|
"snitch-lib",
|
||||||
|
"snitch-web",
|
||||||
|
"snitch-cli",
|
||||||
|
]
|
||||||
|
|
||||||
|
resolver = "2"
|
||||||
|
|||||||
16
snitch-cli/Cargo.toml
Normal file
16
snitch-cli/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "snitch"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
snitch-lib = { path = "../snitch-lib" }
|
||||||
|
|
||||||
|
log = "0.4"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
clap = { version = "4.0.23", features = ["derive", "env"] }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls", "json", "blocking"] }
|
||||||
|
serde_json = "1.0.128"
|
||||||
54
snitch-cli/src/main.rs
Normal file
54
snitch-cli/src/main.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use clap::Parser;
|
||||||
|
use eyre::{eyre, WrapErr};
|
||||||
|
use snitch_lib::{probe_hostname, LogMsg, Severity};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Opt {
|
||||||
|
/// URL of snitch
|
||||||
|
#[clap(long, env = "SNITCH_URL")]
|
||||||
|
url: String,
|
||||||
|
|
||||||
|
/// Name of this service
|
||||||
|
#[clap(long, env = "SNITCH_SERVICE")]
|
||||||
|
service: String,
|
||||||
|
|
||||||
|
/// Name of this service
|
||||||
|
#[clap(short, long, env = "SNITCH_SEVERITY", default_value = "Error")]
|
||||||
|
severity: Severity,
|
||||||
|
|
||||||
|
/// Name of this service
|
||||||
|
#[clap(env = "SNITCH_MESSAGE")]
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(e) = run() {
|
||||||
|
eprintln!("snitch error: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn run() -> eyre::Result<()> {
|
||||||
|
let opt = Opt::parse();
|
||||||
|
color_eyre::install()?;
|
||||||
|
|
||||||
|
let message = LogMsg {
|
||||||
|
time: Some(Local::now()),
|
||||||
|
severity: opt.severity,
|
||||||
|
service: opt.service,
|
||||||
|
message: opt.message,
|
||||||
|
hostname: probe_hostname(),
|
||||||
|
file: None,
|
||||||
|
line: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = serde_json::to_string(&message).wrap_err("Failed to serialize log message")?;
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
client
|
||||||
|
.post(&opt.url)
|
||||||
|
.body(message)
|
||||||
|
.send()
|
||||||
|
.wrap_err(eyre!("Failed to upload log message to {:?}", opt.url))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "snitch"
|
name = "snitch-lib"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@ -8,3 +8,4 @@ log = "0.4"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "serde_json", "blocking", "json"] }
|
reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "serde_json", "blocking", "json"] }
|
||||||
|
eyre = "0.6.12"
|
||||||
@ -1,5 +1,5 @@
|
|||||||
use log::{Level, LevelFilter, Log, Metadata, Record};
|
use log::{Level, LevelFilter, Metadata, Record};
|
||||||
use snitch::SnitchLogger;
|
use snitch_lib::SnitchLogger;
|
||||||
|
|
||||||
struct SimpleLogger;
|
struct SimpleLogger;
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ impl log::Log for SimpleLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let logger = SnitchLogger::new(SimpleLogger, "http://localhost:8000", "snitchlib_example");
|
let logger = SnitchLogger::new(SimpleLogger, "http://localhost:8000", "snitch-lib-example");
|
||||||
let logger = Box::leak(Box::new(logger));
|
let logger = Box::leak(Box::new(logger));
|
||||||
log::set_logger(logger).expect("set logger");
|
log::set_logger(logger).expect("set logger");
|
||||||
log::set_max_level(LevelFilter::Info);
|
log::set_max_level(LevelFilter::Info);
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use std::time::Duration;
|
use std::process::Command;
|
||||||
|
|
||||||
use log::{Level, Log, Metadata, Record};
|
use log::{Level, Log, Metadata, Record};
|
||||||
use reqwest::blocking;
|
use reqwest::blocking;
|
||||||
@ -10,6 +10,7 @@ pub struct SnitchLogger<L: Log> {
|
|||||||
wrapped: L,
|
wrapped: L,
|
||||||
url: String,
|
url: String,
|
||||||
service_name: String,
|
service_name: String,
|
||||||
|
hostname: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<L: Log> SnitchLogger<L> {
|
impl<L: Log> SnitchLogger<L> {
|
||||||
@ -18,20 +19,17 @@ impl<L: Log> SnitchLogger<L> {
|
|||||||
wrapped,
|
wrapped,
|
||||||
url: url.into(),
|
url: url.into(),
|
||||||
service_name: service_name.into(),
|
service_name: service_name.into(),
|
||||||
|
hostname: probe_hostname(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<L: Log> Log for SnitchLogger<L> {
|
impl<L: Log> Log for SnitchLogger<L> {
|
||||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||||
dbg!(self.wrapped.enabled(metadata) || metadata.level() <= Level::Warn)
|
self.wrapped.enabled(metadata) || metadata.level() <= Level::Warn
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &Record) {
|
fn log(&self, record: &Record) {
|
||||||
eprintln!("log called");
|
|
||||||
eprintln!("log called");
|
|
||||||
eprintln!("log called");
|
|
||||||
eprintln!("log called");
|
|
||||||
self.wrapped.log(record);
|
self.wrapped.log(record);
|
||||||
|
|
||||||
let severity = match record.metadata().level() {
|
let severity = match record.metadata().level() {
|
||||||
@ -44,16 +42,15 @@ impl<L: Log> Log for SnitchLogger<L> {
|
|||||||
severity,
|
severity,
|
||||||
file: record.file().map(String::from),
|
file: record.file().map(String::from),
|
||||||
line: record.line(),
|
line: record.line(),
|
||||||
..LogMsg::new(self.service_name.clone(), record.args().to_string())
|
..LogMsg::new(
|
||||||
|
self.service_name.clone(),
|
||||||
|
record.args().to_string(),
|
||||||
|
self.hostname.clone(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = blocking::Client::new();
|
let client = blocking::Client::new();
|
||||||
if let Err(e) = client
|
if let Err(e) = client.post(&self.url).json(&record).send() {
|
||||||
.post(&self.url)
|
|
||||||
.json(&record)
|
|
||||||
.timeout(Duration::from_secs(2))
|
|
||||||
.send()
|
|
||||||
{
|
|
||||||
// TODO: log error (without sending it)
|
// TODO: log error (without sending it)
|
||||||
eprintln!("failed to send log record: {e:?}");
|
eprintln!("failed to send log record: {e:?}");
|
||||||
}
|
}
|
||||||
@ -64,6 +61,16 @@ impl<L: Log> Log for SnitchLogger<L> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn probe_hostname() -> String {
|
||||||
|
let output = Command::new("uname").arg("-n").output().ok();
|
||||||
|
|
||||||
|
output
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|output| std::str::from_utf8(&output.stdout).ok())
|
||||||
|
.unwrap_or("<unknown_hostname>")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
impl<L: Log> Drop for SnitchLogger<L> {
|
impl<L: Log> Drop for SnitchLogger<L> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.flush();
|
self.flush();
|
||||||
78
snitch-lib/src/message.rs
Normal file
78
snitch-lib/src/message.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use eyre::{eyre, ContextCompat};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Severity {
|
||||||
|
Fatal,
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct LogMsg {
|
||||||
|
/// The time that the error was reported.
|
||||||
|
pub time: Option<DateTime<Local>>,
|
||||||
|
|
||||||
|
pub severity: Severity,
|
||||||
|
|
||||||
|
/// The service which reported the error.
|
||||||
|
pub service: String,
|
||||||
|
|
||||||
|
/// The log message.
|
||||||
|
pub message: String,
|
||||||
|
|
||||||
|
/// The host that the service is running on.
|
||||||
|
pub hostname: String,
|
||||||
|
|
||||||
|
pub file: Option<String>,
|
||||||
|
pub line: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogMsg {
|
||||||
|
pub fn new(service: String, message: String, hostname: String) -> Self {
|
||||||
|
Self {
|
||||||
|
time: Some(Local::now()),
|
||||||
|
severity: Severity::Error,
|
||||||
|
service,
|
||||||
|
message,
|
||||||
|
hostname,
|
||||||
|
file: None,
|
||||||
|
line: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Severity {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Severity::Fatal => "FATAL",
|
||||||
|
Severity::Error => "ERROR",
|
||||||
|
Severity::Warning => "WARNING",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Severity {
|
||||||
|
type Err = eyre::Report;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let err = || eyre!("{s:?} is not a valid severity level");
|
||||||
|
|
||||||
|
let first_char = s
|
||||||
|
.trim()
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.wrap_err_with(err)?
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
|
||||||
|
Ok(match first_char {
|
||||||
|
'f' => Severity::Fatal,
|
||||||
|
'e' => Severity::Error,
|
||||||
|
'w' => Severity::Warning,
|
||||||
|
_ => return Err(err()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "snitch_srv"
|
name = "snitch-web"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
snitch = { path = "../snitch" }
|
snitch-lib = { path = "../snitch-lib" }
|
||||||
|
|
||||||
anyhow = "*"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "postgres", "sqlite", "migrate", "macros", "chrono"] }
|
|
||||||
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["handlebars"] }
|
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["handlebars"] }
|
||||||
rocket_db_pools = { version = "0.1.0-rc.2", features = ["sqlx_postgres"] }
|
rocket_db_pools = { version = "0.1.0-rc.2", features = ["sqlx_postgres"] }
|
||||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||||
clap = { version = "4.0.23", features = ["derive", "env"] }
|
clap = { version = "4.0.23", features = ["derive", "env"] }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
redb = { version = "2.1.3", features = ["logging"] }
|
||||||
|
rmp-serde = "1.3.0"
|
||||||
19
snitch-web/src/api.rs
Normal file
19
snitch-web/src/api.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use rocket::{http::Status, serde::json::Json, State};
|
||||||
|
use snitch_lib::LogMsg;
|
||||||
|
|
||||||
|
use crate::database::Database;
|
||||||
|
|
||||||
|
#[post("/", data = "<record>")]
|
||||||
|
pub async fn post_record(db: &State<Database>, record: Json<LogMsg>) -> Status {
|
||||||
|
let mut record = record.into_inner();
|
||||||
|
record.time.get_or_insert_with(Local::now);
|
||||||
|
|
||||||
|
let result = db.write_log(&record);
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("failed to insert record: {e:?}");
|
||||||
|
return Status::InternalServerError;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status::Ok
|
||||||
|
}
|
||||||
@ -1,37 +1,22 @@
|
|||||||
use chrono::{DateTime, Local};
|
|
||||||
use rocket::{http::Status, response::content::RawHtml, State};
|
use rocket::{http::Status, response::content::RawHtml, State};
|
||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{context, Template};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::query;
|
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn index(db: &State<Database>) -> Result<RawHtml<Template>, Status> {
|
pub async fn index(db: &State<Database>) -> Result<RawHtml<Template>, Status> {
|
||||||
let mut conn = db.inner().acquire().await.map_err(|e| {
|
|
||||||
log::error!("failed to get db connection: {e}");
|
|
||||||
Status::InternalServerError
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// maybe only show most recent error for each service in dashboard view, i.e.:
|
// maybe only show most recent error for each service in dashboard view, i.e.:
|
||||||
// select distinct on (service) time, message from (select * from record order by time desc) as foo;
|
// select distinct on (service) time, message from (select * from record order by time desc) as foo;
|
||||||
let messages = query!(
|
let messages = db.get_all_log_messages().map_err(|e| {
|
||||||
"
|
log::error!("failed to query database: {e}");
|
||||||
SELECT time, service, severity, message, file_path, file_line FROM record
|
|
||||||
ORDER BY time DESC;
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.fetch_all(&mut conn)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("failed to query record: {e}");
|
|
||||||
Status::InternalServerError
|
Status::InternalServerError
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Record {
|
struct Record {
|
||||||
time: DateTime<Local>,
|
time: String,
|
||||||
severity: String,
|
severity: String,
|
||||||
service: String,
|
service: String,
|
||||||
message: String,
|
message: String,
|
||||||
@ -40,14 +25,14 @@ SELECT time, service, severity, message, file_path, file_line FROM record
|
|||||||
|
|
||||||
let messages: Vec<_> = messages
|
let messages: Vec<_> = messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|record| Record {
|
.map(|(_id, record)| Record {
|
||||||
time: record.time.into(),
|
time: record.time.map(|time| time.to_string()).unwrap_or_default(),
|
||||||
severity: record.severity,
|
severity: record.severity.as_str().to_string(),
|
||||||
service: record.service,
|
service: record.service,
|
||||||
message: record.message,
|
message: record.message,
|
||||||
location: record
|
location: record
|
||||||
.file_path
|
.file
|
||||||
.zip(record.file_line)
|
.zip(record.line)
|
||||||
.map(|(path, line)| format!("{path}:{line}"))
|
.map(|(path, line)| format!("{path}:{line}"))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
136
snitch-web/src/database.rs
Normal file
136
snitch-web/src/database.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
marker::PhantomData,
|
||||||
|
sync::atomic::{AtomicU64, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use eyre::{eyre, WrapErr};
|
||||||
|
use redb::ReadableTable;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use snitch_lib::LogMsg;
|
||||||
|
|
||||||
|
use crate::Opt;
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
inner: redb::Database,
|
||||||
|
next_log_id: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Serialized<T> {
|
||||||
|
packed: Vec<u8>,
|
||||||
|
_type: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open(opt: &Opt) -> eyre::Result<Database> {
|
||||||
|
let inner = redb::Database::create(&opt.db)
|
||||||
|
.context(eyre!("Failed to open database at {}", opt.db.display()))?;
|
||||||
|
|
||||||
|
let txn = inner.begin_write()?;
|
||||||
|
let next_log_id = txn
|
||||||
|
.open_table(table::LOG_MESSAGE)?
|
||||||
|
.range::<u64>(..)?
|
||||||
|
.next_back()
|
||||||
|
.transpose()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|(id, _)| id.value() + 1)
|
||||||
|
.unwrap_or_default();
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
Ok(Database {
|
||||||
|
inner,
|
||||||
|
next_log_id: next_log_id.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mod table {
|
||||||
|
use super::Serialized;
|
||||||
|
use redb::TableDefinition;
|
||||||
|
use snitch_lib::LogMsg;
|
||||||
|
|
||||||
|
pub const LOG_MESSAGE: TableDefinition<u64, Serialized<LogMsg>> =
|
||||||
|
TableDefinition::new("log_message");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub fn write_log(&self, log: &LogMsg) -> eyre::Result<u64> {
|
||||||
|
let txn = self.inner.begin_write()?;
|
||||||
|
let id = self.next_log_id.fetch_add(1, Ordering::SeqCst);
|
||||||
|
txn.open_table(table::LOG_MESSAGE)?
|
||||||
|
.insert(id, Serialized::serialize(log))?;
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_log_messages(&self) -> eyre::Result<Vec<(u64, LogMsg)>> {
|
||||||
|
let txn = self.inner.begin_read()?;
|
||||||
|
txn.open_table(table::LOG_MESSAGE)?
|
||||||
|
.range::<u64>(..)?
|
||||||
|
.map(|result| {
|
||||||
|
let (id, msg) = result?;
|
||||||
|
let id = id.value();
|
||||||
|
let msg = msg.value();
|
||||||
|
|
||||||
|
msg.deserialize().map(|msg| (id, msg))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Serialized<T>
|
||||||
|
where
|
||||||
|
T: Debug + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
pub fn serialize(value: &T) -> Self {
|
||||||
|
let packed = rmp_serde::to_vec(&value).unwrap();
|
||||||
|
Self {
|
||||||
|
packed,
|
||||||
|
_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize(&self) -> eyre::Result<T> {
|
||||||
|
rmp_serde::from_slice(&self.packed).wrap_err("Failed to deserialize")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> redb::Value for Serialized<T>
|
||||||
|
where
|
||||||
|
T: Debug + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
type SelfType<'a> = Self
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
type AsBytes<'a> = &'a [u8]
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
fn fixed_width() -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
|
||||||
|
where
|
||||||
|
Self: 'a,
|
||||||
|
{
|
||||||
|
Serialized {
|
||||||
|
packed: data.to_vec(),
|
||||||
|
_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
|
||||||
|
where
|
||||||
|
Self: 'a,
|
||||||
|
Self: 'b,
|
||||||
|
{
|
||||||
|
&value.packed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_name() -> redb::TypeName {
|
||||||
|
redb::TypeName::new(std::any::type_name::<Serialized<T>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,27 +5,26 @@ mod api;
|
|||||||
mod dashboard;
|
mod dashboard;
|
||||||
mod database;
|
mod database;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
/// PostgreSQL connect string
|
/// Filepath where snitch will store its database.
|
||||||
///
|
///
|
||||||
/// e.g. postgresql://user:pass@localhost:5432/database
|
/// e.g. postgresql://user:pass@localhost:5432/database
|
||||||
#[clap(
|
#[clap(long, env = "SNITCH_DB", default_value = "./snitch.db")]
|
||||||
long,
|
db: PathBuf,
|
||||||
env = "DATABASE_URL",
|
|
||||||
default_value = "postgresql://postgres@localhost:5432/postgres"
|
|
||||||
)]
|
|
||||||
db: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
let opt = Opt::parse();
|
let opt = Opt::parse();
|
||||||
|
color_eyre::install().unwrap();
|
||||||
|
|
||||||
let db = database::connect(&opt).await.expect("connect to database");
|
let db = database::open(&opt).expect("open database");
|
||||||
|
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.manage(db)
|
.manage(db)
|
||||||
@ -1,39 +0,0 @@
|
|||||||
use chrono::{DateTime, Local};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Severity {
|
|
||||||
Fatal,
|
|
||||||
Error,
|
|
||||||
Warning,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LogMsg {
|
|
||||||
/// The time that the error was reported.
|
|
||||||
pub time: DateTime<Local>,
|
|
||||||
|
|
||||||
pub severity: Severity,
|
|
||||||
|
|
||||||
/// The service which reported the error.
|
|
||||||
pub service: String,
|
|
||||||
|
|
||||||
/// The log message.
|
|
||||||
pub message: String,
|
|
||||||
|
|
||||||
pub file: Option<String>,
|
|
||||||
pub line: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogMsg {
|
|
||||||
pub fn new(service: String, message: String) -> Self {
|
|
||||||
Self {
|
|
||||||
time: Local::now(),
|
|
||||||
severity: Severity::Error,
|
|
||||||
service,
|
|
||||||
message,
|
|
||||||
file: None,
|
|
||||||
line: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
DROP TABLE record;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE record (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
service VARCHAR(255) NOT NULL,
|
|
||||||
severity VARCHAR(16) NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
file_path TEXT,
|
|
||||||
file_line INTEGER
|
|
||||||
|
|
||||||
);
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
use rocket::{http::Status, serde::json::Json, State};
|
|
||||||
use snitch::LogMsg;
|
|
||||||
use sqlx::query;
|
|
||||||
|
|
||||||
use crate::database::Database;
|
|
||||||
|
|
||||||
#[post("/", data = "<record>")]
|
|
||||||
pub async fn post_record(db: &State<Database>, record: Json<LogMsg>) -> Status {
|
|
||||||
let mut conn = match db.inner().acquire().await {
|
|
||||||
Ok(conn) => conn,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("failed to get db connection: {e}");
|
|
||||||
return Status::InternalServerError;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let LogMsg {
|
|
||||||
time: _,
|
|
||||||
severity,
|
|
||||||
service,
|
|
||||||
message,
|
|
||||||
file,
|
|
||||||
line,
|
|
||||||
} = record.into_inner();
|
|
||||||
|
|
||||||
let result = query!(
|
|
||||||
"
|
|
||||||
INSERT INTO record(service,
|
|
||||||
severity,
|
|
||||||
message,
|
|
||||||
file_path,
|
|
||||||
file_line)
|
|
||||||
VALUES ($1, $2, $3, $4, $5);
|
|
||||||
",
|
|
||||||
service,
|
|
||||||
format!("{:?}", severity),
|
|
||||||
message,
|
|
||||||
file,
|
|
||||||
line.map(|line| line as i32),
|
|
||||||
)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
log::error!("failed to insert record: {e:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Status::Ok
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
use crate::Opt;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
pub type Database = PgPool;
|
|
||||||
|
|
||||||
pub(crate) async fn connect(opt: &Opt) -> anyhow::Result<Database> {
|
|
||||||
let pool = PgPool::connect(&opt.db).await?;
|
|
||||||
|
|
||||||
sqlx::migrate!().run(&pool).await?;
|
|
||||||
|
|
||||||
Ok(pool)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user