Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ categories = [
axum = { version = "0.8.4", default-features = false, features = [
"http1",
"tokio",
"json",
] }
clap = "4.5.46"
config = { version = "0.15.15", default-features = false, features = ["toml"] }
Expand Down
134 changes: 131 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,150 @@ Below is the configuration for how Fluxa will behave when running.

```toml
[fluxa]
# Listen on
# Listen on HTTP
listen = "127.0.0.1:8080"

# Optional: Unix socket for API access
api_socket = "/tmp/fluxa.sock"
```

* `listen`: The address and port on which Fluxa will listen. In this example,
Fluxa listens on `127.0.0.1:8080`, meaning it will only accept local connections.
Adjust the address and port as needed.
* `api_socket` (optional): Path to Unix socket for API access. When specified,
Fluxa will create a Unix socket that serves the same API endpoints as the HTTP server.
This is useful for local monitoring tools and provides better security for local access.

#### Fluxa Health Check Endpoint

Fluxa provides an endpoint for cross-monitoring, which can be used to monitor the status of the Fluxa service itself. This endpoint responds with an HTTP status of `200`` and a body of`ok`. You can use this endpoint to monitor Fluxa using another monitoring system.
Fluxa provides an endpoint for cross-monitoring, which can be used to monitor the status of the Fluxa service itself. This endpoint responds with an HTTP status of `200`` and a body of`Ok`. You can use this endpoint to monitor Fluxa using another monitoring system.

Health Check URL: http://<fluxa_host>:<fluxa_port>/
Response:
HTTP Status: 200 OK
Body: Ok

You can use this endpoint to confirm that Fluxa is running and responsive.

#### Real-time Monitoring API

Fluxa exposes a local API for real-time monitoring statistics, enabling you to build Terminal UI (TUI) applications or web interfaces. The API is available via both HTTP and Unix socket (if configured).

**API Endpoints:**

1. **List all monitored services:**
```
GET /api/services
```

**Example Response:**
```json
{
"services": [
{
"url": "https://example.com",
"interval_seconds": 300,
"max_retries": 3,
"retry_interval_seconds": 5,
"stats": {
"status": "Healthy",
"last_response_time_ms": 150,
"last_check_timestamp": 1672531200,
"next_check_timestamp": 1672531500,
"last_error": null,
"current_retry_count": 0
}
}
],
"total_count": 1
}
```

2. **Get individual service details:**
```
GET /api/services/{service_id}
```

The `service_id` can be either:
- Numeric index (0, 1, 2, ...)
- The exact URL of the service

**Example Response:**
```json
{
"url": "https://example.com",
"interval_seconds": 300,
"max_retries": 3,
"retry_interval_seconds": 5,
"stats": {
"status": "Unhealthy",
"last_response_time_ms": null,
"last_check_timestamp": 1672531200,
"next_check_timestamp": 1672531500,
"last_error": "HTTP 500 - Internal Server Error",
"current_retry_count": 2
}
}
```

**API Usage Examples:**

Using HTTP:
```bash
# Get all services
curl http://127.0.0.1:8080/api/services

# Get specific service by index
curl http://127.0.0.1:8080/api/services/0

# Get service by URL (URL-encoded)
curl "http://127.0.0.1:8080/api/services/https%3A%2F%2Fexample.com"
```

Using Unix socket:
```bash
# Get all services
curl --unix-socket /tmp/fluxa.sock http://localhost/api/services

# Get specific service
curl --unix-socket /tmp/fluxa.sock http://localhost/api/services/0
```

**Field Descriptions:**

- `status`: Current health status ("Healthy" or "Unhealthy")
- `last_response_time_ms`: Response time of the last successful request in milliseconds
- `last_check_timestamp`: Unix timestamp of the last health check
- `next_check_timestamp`: Unix timestamp of the next scheduled check
- `last_error`: Description of the last error encountered (null if no errors)
- `current_retry_count`: Number of retry attempts for the current check cycle

**Building TUI/Web Clients:**

The API is designed to be lightweight and easily consumed by monitoring tools:

```bash
# Example: Simple status check script
#!/bin/bash
SOCKET="/tmp/fluxa.sock"
curl -s --unix-socket "$SOCKET" http://localhost/api/services | jq '.services[] | select(.stats.status == "Unhealthy")'
```

```python
# Example: Python client for monitoring
import requests
import json

def get_unhealthy_services():
response = requests.get("http://127.0.0.1:8080/api/services")
data = response.json()
return [s for s in data["services"] if s["stats"]["status"] == "Unhealthy"]
```

Health Check URL: http://<fluxa_host>:<fluxa_port>/
Response:
HTTP Status: 200 OK
Body: ok
Body: Ok

You can use this endpoint to confirm that Fluxa is running and responsive.

Expand Down
15 changes: 11 additions & 4 deletions config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,31 @@ pushover_user_key = "key"
#
# Example
#
# [[Services]]
# [[services]]
# url = "https:/rbas.cz"
# interval_seconds = 300
# max_retries = 3
# retry_interval = 3
#
# [[Services]]
# [[services]]
# url = "https://uptime.kuma.pet/"
# interval_seconds = 300
# max_retries = 3
# retry_interval = 3

[fluxa]
# HTTP server configuration
listen = "127.0.0.1:8080"

# Optional: Unix socket for API access (uncomment to enable)
# api_socket = "/tmp/fluxa.sock"

[[services]]
# Monitored url
url = "http://localhost:3000"
# How ofter the url will be monitored
# How often the url will be monitored
interval_seconds = 300
# Determin how many times it will try before the url will be considered as down
# Determine how many times it will try before the url will be considered as down
max_retries = 3
# How many seconds retry has to wait before next try
retry_interval = 3
108 changes: 104 additions & 4 deletions src/http.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,79 @@
use std::net::SocketAddr;

use axum::{routing::get, Router};
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
routing::get,
Router,
};
use log::info;
use serde_json::{json, Value};

use crate::error::HttpError;
use crate::{
error::HttpError,
model::{MonitoredService, ServiceInfo},
state::{get_all_service_states, get_service_state, SharedMonitoringState},
};

async fn status_handler() -> &'static str {
"Ok"
}

pub async fn spawn_web_server(socket_addr: &str) -> Result<(), HttpError> {
let app = Router::new().route("/", get(status_handler));
async fn get_all_services_handler(
State(state): State<AppState>,
) -> Result<Json<Value>, StatusCode> {
let all_stats = get_all_service_states(&state.monitoring_state).await;
let services: Vec<ServiceInfo> = state
.services
.iter()
.filter_map(|service| {
all_stats
.get(&service.url)
.map(|stats| service.to_service_info(stats.clone()))
})
.collect();

Ok(Json(json!({
"services": services,
"total_count": services.len()
})))
}

async fn get_service_handler(
Path(service_id): Path<String>,
State(state): State<AppState>,
) -> Result<Json<ServiceInfo>, StatusCode> {
// Find service by URL or index
let service = if let Ok(index) = service_id.parse::<usize>() {
state.services.get(index)
} else {
state.services.iter().find(|s| s.url == service_id)
};

let service = service.ok_or(StatusCode::NOT_FOUND)?;
let stats = get_service_state(&state.monitoring_state, &service.url)
.await
.ok_or(StatusCode::NOT_FOUND)?;

Ok(Json(service.to_service_info(stats)))
}

#[derive(Clone)]
pub struct AppState {
pub monitoring_state: SharedMonitoringState,
pub services: Vec<MonitoredService>,
}

pub async fn spawn_web_server(
socket_addr: &str,
app_state: AppState,
) -> Result<(), HttpError> {
let app = Router::new()
.route("/", get(status_handler))
.route("/api/services", get(get_all_services_handler))
.route("/api/services/{service_id}", get(get_service_handler))
.with_state(app_state);

let addr: SocketAddr = socket_addr.parse()?;
info!("Listening on {addr}");
Expand All @@ -25,3 +88,40 @@ pub async fn spawn_web_server(socket_addr: &str) -> Result<(), HttpError> {
}),
}
}

#[cfg(unix)]
pub async fn spawn_unix_socket_server(
socket_path: &str,
app_state: AppState,
) -> Result<(), HttpError> {
use std::path::Path;
use tokio::net::UnixListener;

let app = Router::new()
.route("/", get(status_handler))
.route("/api/services", get(get_all_services_handler))
.route("/api/services/{service_id}", get(get_service_handler))
.with_state(app_state);

// Remove existing socket file if it exists
if Path::new(socket_path).exists() {
std::fs::remove_file(socket_path)
.map_err(|e| HttpError::Server {
message: format!("Failed to remove existing socket file: {}", e),
})?;
}

info!("Listening on Unix socket: {}", socket_path);
let listener = UnixListener::bind(socket_path)
.map_err(|e| HttpError::Server {
message: format!("Failed to bind to Unix socket: {}", e),
})?;

let result = axum::serve(listener, app.into_make_service()).await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(HttpError::Server {
message: e.to_string(),
}),
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod model;
pub mod notification;
pub mod service;
pub mod settings;
pub mod state;
Loading