Skip to content
Open
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
78 changes: 78 additions & 0 deletions .github/workflows/auto-diagnostic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Auto Diagnostic Bundle

on:
push:
branches:
- 'feat/**'
- 'fix/**'
- 'chore/**'

# Skip bot commits to avoid infinite loop
concurrency:
group: diagnostic-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write

jobs:
build-diagnostic:
name: Run build.py and commit diagnostic bundle
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.author.name, 'github-actions')"

steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
gcc g++ cmake make lua5.4 luajit ruby ghc

- name: Make encryptly executable
run: |
chmod +x tools/encryptly/linux-x64/encryptly
chmod +x tools/encryptly/linux-arm64/encryptly || true

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Run build.py
run: python3 build.py
continue-on-error: true

- name: Commit and push diagnostic bundle
run: |
git add diagnostic/ || true
if git diff --cached --quiet; then
echo "No diagnostic files to commit"
exit 0
fi
git commit -m "ci: add diagnostic bundle [skip ci]"
git push origin HEAD
2 changes: 2 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ sha2 = "0.10"
reqwest = { version = "0.12", features = ["json"] }
lazy_static = "1"
log = "0.4"
axum = "0.7"
once_cell = "1.19"

[build-dependencies]
tonic-build = "0.12"
Expand Down
129 changes: 129 additions & 0 deletions backend/src/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Health check endpoint for service monitoring.
//!
//! Provides a `GET /health` endpoint that returns service health information
//! including status, timestamp, version, and uptime.

use axum::{Json, response::IntoResponse};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::time::Instant;

use once_cell::sync::OnceCell;

/// Global startup time for uptime calculation.
static STARTUP_TIME: OnceCell<Instant> = OnceCell::new();

/// Initializes the startup time. Should be called once at application start.
pub fn init_startup_time() {
let _ = STARTUP_TIME.set(Instant::now());
}

/// Returns the uptime in seconds since the service started.
fn get_uptime_seconds() -> u64 {
STARTUP_TIME
.get()
.map(|start| start.elapsed().as_secs())
.unwrap_or(0)
}

/// Health response payload.
///
/// Returned by the `GET /health` endpoint to provide service health information.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HealthResponse {
/// The current health status of the service.
pub status: String,

/// The current UTC timestamp in ISO 8601 format.
pub timestamp: String,

/// The service version from Cargo.toml.
pub version: String,

/// The number of seconds since the service started.
pub uptime_seconds: u64,
}

impl HealthResponse {
/// Creates a new health response with current service state.
pub fn new() -> Self {
Self {
status: "healthy".to_string(),
timestamp: Utc::now().to_rfc3339(),
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds: get_uptime_seconds(),
}
}
}

impl Default for HealthResponse {
fn default() -> Self {
Self::new()
}
}

/// Handler for the `GET /health` endpoint.
///
/// Returns a JSON response containing:
/// - `status`: Service health status ("healthy")
/// - `timestamp`: Current UTC timestamp in ISO 8601 format
/// - `version`: Service version from Cargo.toml
/// - `uptime_seconds`: Seconds since service started
///
/// # Example Response
///
/// ```json
/// {
/// "status": "healthy",
/// "timestamp": "2024-01-15T12:00:00Z",
/// "version": "0.1.0",
/// "uptime_seconds": 3600
/// }
/// ```
pub async fn health_handler() -> impl IntoResponse {
Json(HealthResponse::new())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_health_response_new() {
init_startup_time();
let response = HealthResponse::new();

assert_eq!(response.status, "healthy");
assert_eq!(response.version, env!("CARGO_PKG_VERSION"));
assert!(response.timestamp.len() > 0);
}

#[test]
fn test_health_response_serializes() {
init_startup_time();
let response = HealthResponse::new();
let json = serde_json::to_string(&response).unwrap();

assert!(json.contains("\"status\":\"healthy\""));
assert!(json.contains("\"version\""));
assert!(json.contains("\"timestamp\""));
assert!(json.contains("\"uptime_seconds\""));
}

#[test]
fn test_health_response_deserializes() {
let json = r#"{
"status": "healthy",
"timestamp": "2024-01-15T12:00:00Z",
"version": "0.1.0",
"uptime_seconds": 3600
}"#;

let response: HealthResponse = serde_json::from_str(json).unwrap();

assert_eq!(response.status, "healthy");
assert_eq!(response.timestamp, "2024-01-15T12:00:00Z");
assert_eq!(response.version, "0.1.0");
assert_eq!(response.uptime_seconds, 3600);
}
}
1 change: 1 addition & 0 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod ai;
pub mod config;
pub mod connector;
pub mod discovery;
pub mod health;
pub mod legacy;
pub mod messaging;
pub mod protocol;
Expand Down
8 changes: 4 additions & 4 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ use clap::Parser;
use tent_backend::discovery::ServiceDiscovery;
use tent_backend::messaging::MessageBroker;
use tent_backend::registry::ServiceRegistry;
use tent_backend::health::init_startup_time;
use tracing_subscriber::EnvFilter;

#[derive(Parser, Debug)]
#[command(name = "tent-backend")]
#[command(about = "Tent of Trials Backend - Distributed Microservices Framework", long_about = None)]
struct Cli {

#[arg(short, long, default_value = "node-0")]
node_id: String,

Expand All @@ -24,10 +24,10 @@ struct Cli {
}

#[tokio::main]
// What the fuck is this main function even doing anymore.
// It's 30 lines of config loading and then it spawns a server.
// Actually it's like 50 lines. Still too fucking many.
async fn main() -> Result<()> {
// Initialize startup time for health endpoint uptime tracking
init_startup_time();

tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.json()
Expand Down
86 changes: 86 additions & 0 deletions diagnostic/build-4f063d30.json

Large diffs are not rendered by default.

Binary file added diagnostic/build-4f063d30.logd
Binary file not shown.