added telemetry

feature/production
Mathias Rothenhaeusler 2024-03-29 12:42:22 +01:00
parent b4843108fc
commit 5a18e5728e
24 changed files with 192 additions and 187 deletions

117
Cargo.lock generated
View File

@ -293,17 +293,6 @@ dependencies = [
"quick-xml", "quick-xml",
] ]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -925,19 +914,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "env_logger"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -1129,6 +1105,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "gethostname"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "getopts" name = "getopts"
version = "0.2.21" version = "0.2.21"
@ -1203,15 +1189,6 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.2" version = "0.3.2"
@ -1275,12 +1252,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.27" version = "0.14.27"
@ -1672,7 +1643,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [ dependencies = [
"hermit-abi 0.3.2", "hermit-abi",
"libc", "libc",
] ]
@ -2283,11 +2254,9 @@ dependencies = [
"diesel", "diesel",
"diesel-connection", "diesel-connection",
"dotenv", "dotenv",
"env_logger",
"futures", "futures",
"hmac", "hmac",
"jwt", "jwt",
"log",
"once_cell", "once_cell",
"reqwest", "reqwest",
"rss", "rss",
@ -2298,9 +2267,11 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"tokio", "tokio",
"tracing",
"tracing-actix-web", "tracing-actix-web",
"tracing-appender", "tracing-appender",
"tracing-log", "tracing-bunyan-formatter",
"tracing-log 0.2.0",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
] ]
@ -2709,15 +2680,6 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "thin-slice" name = "thin-slice"
version = "0.1.1" version = "0.1.1"
@ -2903,11 +2865,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.37" version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [ dependencies = [
"cfg-if",
"log", "log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
@ -2951,15 +2912,44 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tracing-core" name = "tracing-bunyan-formatter"
version = "0.1.31" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373"
dependencies = [
"ahash",
"gethostname",
"log",
"serde",
"serde_json",
"time",
"tracing",
"tracing-core",
"tracing-log 0.1.4",
"tracing-subscriber",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
] ]
[[package]]
name = "tracing-log"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]] [[package]]
name = "tracing-log" name = "tracing-log"
version = "0.2.0" version = "0.2.0"
@ -2986,7 +2976,7 @@ dependencies = [
"thread_local", "thread_local",
"tracing", "tracing",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log 0.2.0",
] ]
[[package]] [[package]]
@ -3198,15 +3188,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

@ -29,8 +29,6 @@ uuid = {version = "1.2.1", features=["serde", "v4"]}
jwt = "0.16.0" jwt = "0.16.0"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.6" sha2 = "0.10.6"
log = "0.4.17"
env_logger = "0.9.3"
scraper = "0.14.0" scraper = "0.14.0"
actix-cors = "0.6.4" actix-cors = "0.6.4"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
@ -43,6 +41,8 @@ tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter"]
tracing-log = "0.2.0" tracing-log = "0.2.0"
config = "0.14.0" config = "0.14.0"
diesel-connection = "4.1.0" diesel-connection = "4.1.0"
tracing = { version = "0.1.40", features = ["log"] }
tracing-bunyan-formatter = "0.3.9"
[dependencies.serde_json] [dependencies.serde_json]
version = "1.0.86" version = "1.0.86"

View File

@ -4,6 +4,7 @@ pub mod processes;
use crate::auth::processes::check_password; use crate::auth::processes::check_password;
use crate::auth::processes::extract_header_token; use crate::auth::processes::extract_header_token;
#[tracing::instrument(name = "Process token")]
pub fn process_token(request: &ServiceRequest) -> Result<String, &'static str> { pub fn process_token(request: &ServiceRequest) -> Result<String, &'static str> {
match extract_header_token(request) { match extract_header_token(request) {
Ok(token) => check_password(token), Ok(token) => check_password(token),

View File

@ -1,21 +1,19 @@
use super::jwt; use super::jwt;
use actix_web::dev::ServiceRequest; use actix_web::dev::ServiceRequest;
use secrecy::{ExposeSecret, Secret};
pub fn check_password(password: String) -> Result<String, &'static str> { pub fn check_password(password: Secret<String>) -> Result<String, &'static str> {
match jwt::JwtToken::decode(password) { match jwt::JwtToken::decode(password.expose_secret().to_string()) {
Ok(_token) => Ok(String::from("passed")), Ok(_token) => Ok(String::from("passed")),
Err(message) => Err(message), Err(message) => Err(message),
} }
} }
pub fn extract_header_token(request: &ServiceRequest) -> Result<String, &'static str> { #[tracing::instrument(name = "Extract Header Token")]
log::info!("Request: {:?}", request); pub fn extract_header_token(request: &ServiceRequest) -> Result<Secret<String>, &'static str> {
match request.headers().get("user-token") { match request.headers().get("user-token") {
Some(token) => match token.to_str() { Some(token) => match token.to_str() {
Ok(processed_password) => { Ok(processed_password) => Ok(Secret::new(String::from(processed_password))),
log::info!("Token provided: {}", processed_password);
Ok(String::from(processed_password))
}
Err(_processed_password) => Err("there was an error processing token"), Err(_processed_password) => Err("there was an error processing token"),
}, },
None => Err("there is no token"), None => Err("there is no token"),
@ -25,6 +23,7 @@ pub fn extract_header_token(request: &ServiceRequest) -> Result<String, &'static
#[cfg(test)] #[cfg(test)]
mod processes_test { mod processes_test {
use actix_web::test::TestRequest; use actix_web::test::TestRequest;
use secrecy::{ExposeSecret, Secret};
use crate::auth::jwt::JwtToken; use crate::auth::jwt::JwtToken;
@ -32,7 +31,7 @@ mod processes_test {
#[test] #[test]
fn check_correct_password() { fn check_correct_password() {
let password_string: String = JwtToken::encode(32); let password_string: Secret<String> = Secret::new(JwtToken::encode(32));
let result = check_password(password_string); let result = check_password(password_string);
@ -44,7 +43,7 @@ mod processes_test {
#[test] #[test]
fn incorrect_check_password() { fn incorrect_check_password() {
let password: String = String::from("test"); let password: Secret<String> = Secret::new(String::from("test"));
match check_password(password) { match check_password(password) {
Err(message) => assert_eq!("could not decode token", message), Err(message) => assert_eq!("could not decode token", message),
@ -59,7 +58,7 @@ mod processes_test {
.to_srv_request(); .to_srv_request();
match super::extract_header_token(&request) { match super::extract_header_token(&request) {
Ok(processed_password) => assert_eq!("token", processed_password), Ok(processed_password) => assert_eq!("token", processed_password.expose_secret()),
_ => panic!("failed extract_header_token"), _ => panic!("failed extract_header_token"),
} }
} }

View File

@ -1,6 +1,6 @@
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub struct NewFeedSchema { pub struct NewFeedSchema {
pub title: String, pub title: String,
pub url: String, pub url: String,

View File

@ -1,6 +1,6 @@
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub struct NewUserSchema { pub struct NewUserSchema {
pub name: String, pub name: String,
pub email: String, pub email: String,

View File

@ -1,6 +1,6 @@
use serde_derive::Deserialize; use serde_derive::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub struct ReadItem { pub struct ReadItem {
pub id: i32, pub id: i32,
} }

View File

@ -1,6 +1,6 @@
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub struct UrlJson { pub struct UrlJson {
pub url: String, pub url: String,
} }

View File

@ -1,6 +1,6 @@
use serde_derive::Deserialize; use serde_derive::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub struct JsonUser { pub struct JsonUser {
pub user_id: i32, pub user_id: i32,
} }

View File

@ -9,4 +9,5 @@ pub mod models;
pub mod reader; pub mod reader;
pub mod schema; pub mod schema;
pub mod startup; pub mod startup;
pub mod telemetry;
pub mod views; pub mod views;

View File

@ -4,12 +4,18 @@ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use rss_reader::{configuration::get_configuration, database::get_connection_pool, startup::run}; use rss_reader::{
configuration::get_configuration,
database::get_connection_pool,
startup::run,
telemetry::{get_subscriber, init_subscriber},
};
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::init(); let subscriber = get_subscriber("zero2prod".into(), "info".into(), std::io::stdout);
init_subscriber(subscriber);
let configuration = get_configuration().expect("Failed to read configuration."); let configuration = get_configuration().expect("Failed to read configuration.");

View File

@ -2,6 +2,7 @@ extern crate bcrypt;
use bcrypt::{hash, DEFAULT_COST}; use bcrypt::{hash, DEFAULT_COST};
use diesel::Insertable; use diesel::Insertable;
use secrecy::{ExposeSecret, Secret};
use uuid::Uuid; use uuid::Uuid;
use crate::schema::users; use crate::schema::users;
@ -16,8 +17,9 @@ pub struct NewUser {
} }
impl NewUser { impl NewUser {
pub fn new(username: String, email: String, password: String) -> NewUser { pub fn new(username: String, email: String, password: Secret<String>) -> NewUser {
let hashed_password: String = hash(password.as_str(), DEFAULT_COST).unwrap(); let hashed_password: String =
hash(password.expose_secret().as_str(), DEFAULT_COST).unwrap();
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
NewUser { NewUser {
username, username,

View File

@ -10,6 +10,7 @@ use crate::{
use super::feeds; use super::feeds;
#[tracing::instrument(name = "Add new feed", skip(pool))]
pub async fn add( pub async fn add(
new_feed: web::Json<NewFeedSchema>, new_feed: web::Json<NewFeedSchema>,
pool: web::Data<Pool<ConnectionManager<PgConnection>>>, pool: web::Data<Pool<ConnectionManager<PgConnection>>>,
@ -24,13 +25,11 @@ pub async fn add(
let result = feeds::get_feed(&url).await; let result = feeds::get_feed(&url).await;
match result { match result {
Ok(channel) => { Ok(channel) => {
log::info!("valid channel");
if channel.items.is_empty() { if channel.items.is_empty() {
return HttpResponse::ServiceUnavailable().await.unwrap(); return HttpResponse::ServiceUnavailable().await.unwrap();
} }
} }
Err(e) => { Err(_) => {
log::error!("{:?}", e);
return HttpResponse::NotFound().await.unwrap(); return HttpResponse::NotFound().await.unwrap();
} }
} }
@ -43,9 +42,6 @@ pub async fn add(
match insert_result { match insert_result {
Ok(_) => HttpResponse::Created().await.unwrap(), Ok(_) => HttpResponse::Created().await.unwrap(),
Err(e) => { Err(_) => HttpResponse::Conflict().await.unwrap(),
log::error!("{e}");
HttpResponse::Conflict().await.unwrap()
}
} }
} }

View File

@ -2,9 +2,9 @@ use std::error::Error;
use rss::Channel; use rss::Channel;
#[tracing::instrument(name = "Get Channel Feed")]
pub async fn get_feed(feed: &str) -> Result<Channel, Box<dyn Error>> { pub async fn get_feed(feed: &str) -> Result<Channel, Box<dyn Error>> {
let content = reqwest::get(feed).await?.bytes().await?; let content = reqwest::get(feed).await?.bytes().await?;
let channel = Channel::read_from(&content[..])?; let channel = Channel::read_from(&content[..])?;
log::debug!("{:?}", channel);
Ok(channel) Ok(channel)
} }

View File

@ -10,11 +10,12 @@ use crate::{
}; };
use actix_web::{web, HttpRequest, Responder}; use actix_web::{web, HttpRequest, Responder};
use chrono::Local; use chrono::Local;
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool}; use diesel::r2d2::{ConnectionManager, Pool};
use diesel::{prelude::*, r2d2};
use super::structs::article::Article; use super::structs::article::Article;
#[tracing::instrument(name = "Get feeds", skip(pool))]
pub async fn get( pub async fn get(
path: web::Path<JsonUser>, path: web::Path<JsonUser>,
req: HttpRequest, req: HttpRequest,
@ -22,7 +23,6 @@ pub async fn get(
) -> impl Responder { ) -> impl Responder {
let request = req.clone(); let request = req.clone();
let req_user_id = path.user_id; let req_user_id = path.user_id;
log::info!("Received user_id: {}", req_user_id);
// Clone the Arc containing the connection pool // Clone the Arc containing the connection pool
let pool_arc = pool.get_ref().clone(); let pool_arc = pool.get_ref().clone();
// Acquire a connection from the pool // Acquire a connection from the pool
@ -35,42 +35,7 @@ pub async fn get(
let mut feed_aggregates: Vec<FeedAggregate> = Vec::new(); let mut feed_aggregates: Vec<FeedAggregate> = Vec::new();
for feed in feeds { for feed in feeds {
let existing_item: Vec<FeedItem> = feed_item::table feed_aggregates.push(get_feed_aggregate(feed, &mut connection))
.filter(feed_id.eq(feed.id))
.filter(read.eq(false))
.order(id.asc())
.load(&mut connection)
.unwrap();
log::info!(
"Load {} feed items for feed: {}",
existing_item.len(),
feed.url
);
let article_list: Vec<Article> = existing_item
.into_iter()
.map(|feed_item: FeedItem| {
let time: String = match feed_item.created_ts {
Some(r) => r.to_string(),
None => Local::now().naive_local().to_string(),
};
Article {
title: feed_item.title,
content: feed_item.content,
url: feed_item.url,
timestamp: time,
id: feed_item.id,
}
})
.collect();
log::info!("article list with {} items generated.", article_list.len());
feed_aggregates.push(FeedAggregate {
title: feed.title,
items: article_list,
})
} }
let articles: Articles = Articles { let articles: Articles = Articles {
@ -79,3 +44,38 @@ pub async fn get(
articles.respond_to(&request) articles.respond_to(&request)
} }
#[tracing::instrument(name = "Get feed aggregate", skip(connection))]
pub fn get_feed_aggregate(
feed: Feed,
connection: &mut r2d2::PooledConnection<ConnectionManager<PgConnection>>,
) -> FeedAggregate {
let existing_item: Vec<FeedItem> = feed_item::table
.filter(feed_id.eq(feed.id))
.filter(read.eq(false))
.order(id.asc())
.load(connection)
.unwrap();
let article_list: Vec<Article> = existing_item
.into_iter()
.map(|feed_item: FeedItem| {
let time: String = match feed_item.created_ts {
Some(r) => r.to_string(),
None => Local::now().naive_local().to_string(),
};
Article {
title: feed_item.title,
content: feed_item.content,
url: feed_item.url,
timestamp: time,
id: feed_item.id,
}
})
.collect();
FeedAggregate {
title: feed.title,
items: article_list,
}
}

View File

@ -3,36 +3,34 @@ use crate::{
json_serialization::read_feed_item::ReadItem, models::feed_item::rss_feed_item::FeedItem, json_serialization::read_feed_item::ReadItem, models::feed_item::rss_feed_item::FeedItem,
schema::feed_item, schema::feed_item,
}; };
use actix_web::{web, HttpRequest, HttpResponse, Responder}; use actix_web::{web, HttpRequest, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool}; use diesel::r2d2::{ConnectionManager, Pool};
use diesel::{ExpressionMethods, QueryDsl}; use diesel::{ExpressionMethods, QueryDsl};
use diesel::{PgConnection, RunQueryDsl}; use diesel::{PgConnection, RunQueryDsl};
#[tracing::instrument(name = "Mark as read", skip(pool))]
pub async fn mark_read( pub async fn mark_read(
_req: HttpRequest, _req: HttpRequest,
path: web::Path<ReadItem>, path: web::Path<ReadItem>,
pool: web::Data<Pool<ConnectionManager<PgConnection>>>, pool: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> impl Responder { ) -> HttpResponse {
let pool_arc = pool.get_ref().clone(); let pool_arc = pool.get_ref().clone();
let mut connection = pool_arc.get().expect("Failed to get database connection"); let mut connection = pool_arc.get().expect("Failed to get database connection");
log::info!("Id: {}", path.id);
let feed_items: Vec<FeedItem> = feed_item::table let feed_items: Vec<FeedItem> = feed_item::table
.filter(id.eq(path.id)) .filter(id.eq(path.id))
.load::<FeedItem>(&mut connection) .load::<FeedItem>(&mut connection)
.unwrap(); .unwrap();
if feed_items.len() != 1 { if feed_items.len() != 1 {
return HttpResponse::NotFound(); return HttpResponse::NotFound().await.unwrap();
} }
let feed_item: &FeedItem = feed_items.first().unwrap(); let feed_item: &FeedItem = feed_items.first().unwrap();
let result: Result<usize, diesel::result::Error> = diesel::update(feed_item) let _result: Result<usize, diesel::result::Error> = diesel::update(feed_item)
.set(read.eq(true)) .set(read.eq(true))
.execute(&mut connection); .execute(&mut connection);
log::info!("Mark as read: {:?}", result); HttpResponse::Ok().await.unwrap()
HttpResponse::Ok()
} }

View File

@ -4,15 +4,13 @@ use crate::json_serialization::{readable::Readable, url::UrlJson};
use super::scraper::content::do_throttled_request; use super::scraper::content::do_throttled_request;
#[tracing::instrument(name = "Read Feed")]
pub async fn read(_req: HttpRequest, data: web::Json<UrlJson>) -> impl Responder { pub async fn read(_req: HttpRequest, data: web::Json<UrlJson>) -> impl Responder {
let result = do_throttled_request(&data.url); let result = do_throttled_request(&data.url);
let content = match result.await { let content = match result.await {
Ok(cont) => cont, Ok(cont) => cont,
Err(e) => { Err(e) => e.to_string(),
log::error!("Could not scrap url {}", data.url);
e.to_string()
}
}; };
Readable { content } Readable { content }

View File

@ -8,7 +8,7 @@ use crate::schema::{
feed::{self, user_id}, feed::{self, user_id},
feed_item, feed_item,
}; };
use actix_web::{web, HttpRequest, HttpResponse, Responder}; use actix_web::{web, HttpRequest, HttpResponse};
use chrono::{DateTime, Local, NaiveDateTime}; use chrono::{DateTime, Local, NaiveDateTime};
use dateparser::parse; use dateparser::parse;
use diesel::prelude::*; use diesel::prelude::*;
@ -16,12 +16,12 @@ use diesel::r2d2::{ConnectionManager, Pool};
use rss::Item; use rss::Item;
use scraper::{Html, Selector}; use scraper::{Html, Selector};
#[tracing::instrument(name = "Get Date")]
fn get_date(date_str: &str) -> Result<NaiveDateTime, chrono::ParseError> { fn get_date(date_str: &str) -> Result<NaiveDateTime, chrono::ParseError> {
// let format_string = "%a, %d %b %Y %H:%M:%S %z"; // let format_string = "%a, %d %b %Y %H:%M:%S %z";
let format_string = "%Y-%m-%dT%H:%M:%S%Z"; let format_string = "%Y-%m-%dT%H:%M:%S%Z";
let result = parse(date_str).unwrap(); let result = parse(date_str).unwrap();
log::info!("Date: {:?}", result);
match NaiveDateTime::parse_from_str(&result.to_string(), format_string) { match NaiveDateTime::parse_from_str(&result.to_string(), format_string) {
Ok(r) => Ok(r), Ok(r) => Ok(r),
@ -38,9 +38,9 @@ fn get_date(date_str: &str) -> Result<NaiveDateTime, chrono::ParseError> {
} }
} }
#[tracing::instrument(name = "Create Feed Item", skip(connection))]
fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) { fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) {
let item_title = item.title.clone().unwrap(); let item_title = item.title.clone().unwrap();
log::info!("Create feed item: {}", item_title);
let base_content: &str = match item.content() { let base_content: &str = match item.content() {
Some(c) => c, Some(c) => c,
@ -74,15 +74,11 @@ fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) {
.unwrap(); .unwrap();
if existing_item.is_empty() { if existing_item.is_empty() {
log::info!("{:?}", item.pub_date());
let mut time: NaiveDateTime = Local::now().naive_local(); let mut time: NaiveDateTime = Local::now().naive_local();
if item.pub_date().is_some() { if item.pub_date().is_some() {
time = match get_date(item.pub_date().unwrap()) { time = match get_date(item.pub_date().unwrap()) {
Ok(date) => date, Ok(date) => date,
Err(err) => { Err(_err) => time,
log::error!("could not unwrap pub date: {}", err);
time
}
}; };
} }
let new_feed_item = NewFeedItem::new( let new_feed_item = NewFeedItem::new(
@ -92,21 +88,18 @@ fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) {
item.link.unwrap(), item.link.unwrap(),
Some(time), Some(time),
); );
let insert_result = diesel::insert_into(feed_item::table) let _insert_result = diesel::insert_into(feed_item::table)
.values(&new_feed_item) .values(&new_feed_item)
.execute(connection); .execute(connection);
log::info!("Insert Result: {:?}", insert_result);
} else {
log::info!("Item {} already exists.", item_title);
} }
} }
#[tracing::instrument(name = "sync", skip(pool))]
pub async fn sync( pub async fn sync(
_req: HttpRequest, _req: HttpRequest,
data: web::Json<JsonUser>, data: web::Json<JsonUser>,
pool: web::Data<Pool<ConnectionManager<PgConnection>>>, pool: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> impl Responder { ) -> HttpResponse {
let pool_arc = pool.get_ref().clone(); let pool_arc = pool.get_ref().clone();
let mut connection = pool_arc.get().expect("Failed to get database connection"); let mut connection = pool_arc.get().expect("Failed to get database connection");
@ -117,22 +110,18 @@ pub async fn sync(
.load::<Feed>(&mut connection) .load::<Feed>(&mut connection)
.unwrap(); .unwrap();
log::info!("Found {} feeds to sync.", feeds.len());
for feed in feeds { for feed in feeds {
log::info!("Try to get url: {}", feed.url);
let result = feeds::get_feed(&feed.url).await; let result = feeds::get_feed(&feed.url).await;
match result { match result {
Ok(channel) => { Ok(channel) => {
for item in channel.into_items() { for item in channel.into_items() {
log::info!("{:?}", item);
create_feed_item(item, &feed, &mut connection); create_feed_item(item, &feed, &mut connection);
} }
} }
Err(e) => log::error!("Could not get channel {}. Error: {}", feed.url, e), Err(_e) => return HttpResponse::InternalServerError().await.unwrap(),
} }
} }
HttpResponse::Ok() HttpResponse::Ok().await.unwrap()
} }

View File

@ -10,25 +10,23 @@ use futures::future::{ok, Either};
use crate::auth; use crate::auth;
use crate::views; use crate::views;
#[tracing::instrument(name = "Run application", skip(connection, listener))]
pub fn run( pub fn run(
listener: TcpListener, listener: TcpListener,
connection: Pool<ConnectionManager<PgConnection>>, connection: Pool<ConnectionManager<PgConnection>>,
) -> Result<Server, std::io::Error> { ) -> Result<Server, std::io::Error> {
let wrapper = web::Data::new(connection); let wrapper = web::Data::new(connection);
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
let app = App::new() App::new()
.wrap_fn(|req, srv| { .wrap_fn(|req, srv| {
let mut passed: bool; let mut passed: bool;
let request_url: String = String::from(req.uri().path());
log::info!("Request Url: {}", request_url);
if req.path().contains("/article/") { if req.path().contains("/article/") {
match auth::process_token(&req) { match auth::process_token(&req) {
Ok(_token) => passed = true, Ok(_token) => passed = true,
Err(_message) => passed = false, Err(_message) => passed = false,
} }
} else { } else {
log::warn!("No auth check done.");
passed = true; passed = true;
} }
@ -36,8 +34,6 @@ pub fn run(
passed = true; passed = true;
} }
log::info!("passed: {:?}", passed);
let end_result = match passed { let end_result = match passed {
true => Either::Left(srv.call(req)), true => Either::Left(srv.call(req)),
false => Either::Right(ok(req.into_response( false => Either::Right(ok(req.into_response(
@ -47,13 +43,11 @@ pub fn run(
async move { async move {
let result = end_result.await?; let result = end_result.await?;
log::info!("{} -> {}", request_url, &result.status());
Ok(result) Ok(result)
} }
}) })
.app_data(wrapper.clone()) .app_data(wrapper.clone())
.configure(views::views_factory); .configure(views::views_factory)
app
}) })
.listen(listener)? .listen(listener)?
.run(); .run();

27
src/telemetry.rs 100644
View File

@ -0,0 +1,27 @@
use tracing::{dispatcher::set_global_default, Subscriber};
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt::MakeWriter, layer::SubscriberExt, EnvFilter, Registry};
pub fn get_subscriber<Sink>(
name: String,
env_filter: String,
sink: Sink,
) -> impl Subscriber + Send + Sync
where
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter));
let formatting_layer = BunyanFormattingLayer::new(name, sink);
Registry::default()
.with(env_filter)
.with(JsonStorageLayer)
.with(formatting_layer)
}
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
LogTracer::init().expect("Failed to set logger.");
set_global_default(subscriber.into()).expect("Failed to set subscriber.");
}

View File

@ -25,10 +25,6 @@ pub async fn login(
if users.is_empty() { if users.is_empty() {
return HttpResponse::NotFound().await.unwrap(); return HttpResponse::NotFound().await.unwrap();
} else if users.len() > 1 { } else if users.len() > 1 {
log::error!(
"multiple user have the usernam: {}",
credentials.username.clone()
);
return HttpResponse::Conflict().await.unwrap(); return HttpResponse::Conflict().await.unwrap();
} }
@ -36,7 +32,6 @@ pub async fn login(
match user.clone().verify(password) { match user.clone().verify(password) {
true => { true => {
log::info!("verified password successfully for user {}", user.id);
let token: String = JwtToken::encode(user.clone().id); let token: String = JwtToken::encode(user.clone().id);
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(("token", token)) .insert_header(("token", token))

View File

@ -7,7 +7,9 @@ use diesel::{
prelude::*, prelude::*,
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
}; };
use secrecy::Secret;
#[tracing::instrument(name = "Create new User", skip(pool))]
pub async fn create( pub async fn create(
new_user: web::Json<NewUserSchema>, new_user: web::Json<NewUserSchema>,
pool: web::Data<Pool<ConnectionManager<PgConnection>>>, pool: web::Data<Pool<ConnectionManager<PgConnection>>>,
@ -17,7 +19,7 @@ pub async fn create(
let name: String = new_user.name.clone(); let name: String = new_user.name.clone();
let email: String = new_user.email.clone(); let email: String = new_user.email.clone();
let new_password: String = new_user.password.clone(); let new_password: Secret<String> = Secret::new(new_user.password.clone());
let new_user = NewUser::new(name, email, new_password); let new_user = NewUser::new(name, email, new_password);

View File

@ -45,18 +45,33 @@ a,
font-family: Georgia, 'Times New Roman', Times, serif; font-family: Georgia, 'Times New Roman', Times, serif;
font-size: 20px; font-size: 20px;
padding: 1em; padding: 1em;
display: flex;
flex-direction: column;
align-items: left;
text-align: left;
} }
.feed-content p { .feed-content p {
padding: 1em; padding: 1em;
} }
.feed-content h3 { .feed-content h2,
h3,
h4,
h5,
h6 {
padding: 1em; padding: 1em;
font-size: 21px; font-size: 21px;
font-weight: bold; font-weight: bold;
} }
.feed-content img {
max-width: 100%;
margin-bottom: 10px;
/* Adjust spacing between image and text */
}
h3 { h3 {
font-size: 14px; font-size: 14px;
} }

View File

@ -67,9 +67,10 @@ const fetchData = async () => {
'user-token': localStorage.getItem("user-token") 'user-token': localStorage.getItem("user-token")
} }
}); });
response.data.feeds.forEach(feed => { const sortedItems = response.data.feeds.flatMap(feed => feed.items)
feeds.value.push(...feed.items); .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
});
feeds.value.push(...sortedItems);
await nextTick(); await nextTick();
setupIntersectionObserver(); setupIntersectionObserver();
} catch (error) { } catch (error) {