Initial commit
This commit is contained in:
commit
915dd13e74
8 changed files with 2173 additions and 0 deletions
4
.env.example
Normal file
4
.env.example
Normal 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
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
dev/
|
||||||
|
.env
|
2011
Cargo.lock
generated
Normal file
2011
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal 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
10
README.md
Normal 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
70
src/main.rs
Normal 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
29
src/submit.rs
Normal 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
30
src/vars.rs
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue