Initial commit

This commit is contained in:
Sofía Aritz 2023-04-01 14:31:58 +02:00
commit 915dd13e74
8 changed files with 2173 additions and 0 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
LISTEN_STR=0.0.0.0:8080
CORS_HOST=*
EXPOSE_VERSION=true
SQLITE_PATH=./dev/db.sqlite

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target/
.idea/
dev/
.env

2011
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "simple-analytics"
version = "0.1.0"
edition = "2021"
[dependencies]
femme = "2"
anyhow = "1"
serde_json = "1"
tide = "0.16"
dotenv = "0.15"
rusqlite = { version = "0.28", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
async-std = { version = "1", features = ["attributes"] }
uuid = { version = "1", features = ["v4", "fast-rng"] }

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# Simple Analytics
A flexible and simple analytics system with a simple API that allows everyone to create simple, fast and private
analytics.
## Usage
1. Create a `.env` file based on the `.env.example` file.
2. Run the `cargo run` command.
3. Send a POST request to `/submit/{event_name}` with a JSON payload.

70
src/main.rs Normal file
View file

@ -0,0 +1,70 @@
use std::sync::{Arc, Mutex};
use femme::LevelFilter;
use rusqlite::Connection;
use tide::http::headers::HeaderValue;
use tide::log::{info, LogMiddleware};
use tide::{Response, StatusCode};
use tide::security::{CorsMiddleware, Origin};
use crate::submit::submit_event;
use crate::vars::{cors_host, expose_version, listen_str, sqlite_path};
mod vars;
mod submit;
#[derive(Clone)]
pub struct State(Arc<Mutex<Connection>>);
#[async_std::main]
async fn main() {
femme::with_level(LevelFilter::Info);
info!("version: {}", env!("CARGO_PKG_VERSION"));
if let Ok(path) = dotenv::dotenv() {
info!("successfully loaded environment variables from: {:?}", path);
info!("environment variables: LISTEN_STR={}; CORS_HOST={}; EXPOSE_VERSION={}", listen_str(), cors_host(), expose_version());
} else {
panic!("couldn't load environment variables from .env");
}
let cors = CorsMiddleware::new()
.allow_methods("GET, POST, OPTIONS".parse::<HeaderValue>().unwrap())
.allow_origin(Origin::from(cors_host()))
.allow_credentials(false);
let connection = Connection::open(sqlite_path())
.expect("failed to open a connection with the SQLite database");
connection.execute(
"CREATE TABLE IF NOT EXISTS analytics (\
id TEXT PRIMARY KEY,
event_name TEXT NOT NULL,
data TEXT NOT NULL
)",
())
.expect("failed to create the table");
let mut app = tide::with_state(State(Arc::new(Mutex::new(connection))));
app.with(cors);
app.with(LogMiddleware::new());
if expose_version() {
app.at("/").get(|_| async {
Ok(Response::builder(StatusCode::Ok)
.header("Content-Type", "text/html")
.body(format!("<a href=\"https://git.sofiaritz.com/sofia/simple-analytics\">Simple Analytics</a>: A flexible and simple analytics system. (Version: {})", env!("CARGO_PKG_VERSION"))))
});
} else {
app.at("/").get(|_| async {
Ok(Response::builder(StatusCode::Ok)
.header("Content-Type", "text/html")
.body("<a href=\"https://git.sofiaritz.com/sofia/simple-analytics\">Simple Analytics</a>: A flexible and simple analytics system."))
});
}
app.at("/submit/:event_name").post(submit_event);
app.listen(&listen_str()).await
.expect("failed to start the server");
}

29
src/submit.rs Normal file
View file

@ -0,0 +1,29 @@
use serde_json::Value;
use tide::log::error;
use tide::{Request, Response, StatusCode};
use uuid::Uuid;
use crate::State;
pub async fn submit_event(mut req: Request<State>) -> tide::Result {
let json_values: Value = req.body_json().await?;
let event_name = req.param("event_name")?;
let text_value = serde_json::to_string(&json_values).unwrap();
let request_id = Uuid::new_v4().to_string();
{
let state = req.state().clone();
let connection = state.0.lock().expect("failed to get read lock");
if let Err(err) = connection.execute(
"INSERT INTO analytics (id, event_name, data) VALUES (?1, ?2, ?3)",
(&request_id, &event_name, &text_value)
) {
error!("failed to execute insert query: {}", err);
error!("input of failed query: id={}; event_name={}; data={}", request_id, event_name, text_value);
return Ok(Response::new(StatusCode::InternalServerError));
}
}
Ok(Response::new(StatusCode::Ok))
}

30
src/vars.rs Normal file
View file

@ -0,0 +1,30 @@
use std::env;
use std::path::PathBuf;
pub fn listen_str() -> String {
env::var("LISTEN_STR").expect("LISTEN_STR environment variable doesn't exist")
}
pub fn cors_host() -> String {
env::var("CORS_HOST").expect("CORS_HOST environment variable doesn't exist")
}
pub fn expose_version() -> bool {
let raw_value = env::var("EXPOSE_VERSION").expect("EXPOSE_VERSION environment variable doesn't exist");
match raw_value.as_str() {
"1" => true,
"true" => true,
"yes" => true,
"0" => false,
"false" => false,
"no" => false,
_ => panic!("invalid value in the EXPOSE_VERSION environment variable"),
}
}
/// # Notes
/// This function doesn't guarantee that the path is valid
pub fn sqlite_path() -> PathBuf {
let raw_value = env::var("SQLITE_PATH").expect("SQLITE_PATH environment variable doesn't exist");
PathBuf::from(raw_value)
}