ordinary-api 0.6.0-pre.13

API server for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::ApiInfo;
use crate::server::ops::{
    LogFilesParams, LogMetadataParams, log_files_helper, log_files_metadata_helper,
};
use crate::server::{OrdinaryApiServerState, ROOT};
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use hyper::HeaderMap;
use ordinary_monitor::LogLine;
use serde::Deserialize;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use tracing::Instrument;
use utoipa::IntoParams;

#[utoipa::path(
    get,
    path = "/logs/metadata",
    tag = ROOT,
    params(LogMetadataParams),
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "log files metadata", body = [LogLine]),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn log_files_metadata(
    State(state): State<Arc<OrdinaryApiServerState>>,
    Query(query): Query<LogMetadataParams>,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("root");
    let span = span.in_scope(|| tracing::info_span!("logs"));

    async {
        match crate::server::check_ordinary_auth(&state, &headers, 1, "root") {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        log_files_metadata_helper(&state, &query)
            .await
            .into_response()
    }
    .instrument(span)
    .await
}

#[utoipa::path(
    get,
    path = "/logs/files/{file}",
    tags = [ROOT],
    params(LogFilesParams),
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "jsonl log lines compressed with Zstd", body = [LogLine]),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn log_files(
    State(state): State<Arc<OrdinaryApiServerState>>,
    Query(query): Query<LogFilesParams>,
    Path(file_name): Path<String>,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("root", domain = %query.d);
    let span = span.in_scope(|| tracing::info_span!("logs"));

    async move {
        match crate::server::check_ordinary_auth(&state, &headers, 1, "root") {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        log_files_helper(&state, &query, file_name.as_str())
            .await
            .into_response()
    }
    .instrument(span)
    .await
}

struct CpuTimeDisplay(u64);

impl Display for CpuTimeDisplay {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}ms", self.0)
    }
}

#[allow(dead_code)]
struct CpuPercentageDisplay(f32);

impl Display for CpuPercentageDisplay {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:.2}%", self.0)
    }
}

#[utoipa::path(
    get,
    path = "/info",
    tags = [ROOT],
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "API system info", body = [ApiInfo]),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn info(
    State(state): State<Arc<OrdinaryApiServerState>>,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("root");
    let span = span.in_scope(|| tracing::info_span!("info"));

    span.in_scope(|| {
        match crate::server::check_ordinary_auth(&state, &headers, 1, "root") {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        let api_info = {
            let mut sys = state.system.lock();

            sys.refresh_all();

            if let Some(process) = sys.process(state.pid) {
                let cpu_ct = sys.cpus().len();
                let du = process.disk_usage();

                Some(ApiInfo {
                    system_cpu_count: cpu_ct,
                    system_cpu_usage: CpuPercentageDisplay(sys.global_cpu_usage()).to_string(),

                    system_memory_total: bytesize::ByteSize(sys.total_memory())
                        .display()
                        .si_short()
                        .to_string(),
                    system_memory_used: bytesize::ByteSize(sys.used_memory())
                        .display()
                        .si_short()
                        .to_string(),
                    system_memory_free: bytesize::ByteSize(sys.free_memory())
                        .display()
                        .si_short()
                        .to_string(),
                    system_memory_available: bytesize::ByteSize(sys.available_memory())
                        .display()
                        .si_short()
                        .to_string(),

                    process_cpu_usage: CpuPercentageDisplay(process.cpu_usage() / cpu_ct as f32)
                        .to_string(),
                    process_cpu_time: CpuTimeDisplay(process.accumulated_cpu_time()).to_string(),

                    process_threads: num_threads::num_threads().map(usize::from),

                    process_memory_real: bytesize::ByteSize(process.memory())
                        .display()
                        .si_short()
                        .to_string(),
                    process_memory_virtual: bytesize::ByteSize(process.virtual_memory())
                        .display()
                        .si_short()
                        .to_string(),

                    process_disk_read: bytesize::ByteSize(du.total_read_bytes)
                        .display()
                        .si_short()
                        .to_string(),
                    process_disk_write: bytesize::ByteSize(du.total_written_bytes)
                        .display()
                        .si_short()
                        .to_string(),
                })
            } else {
                None
            }
        };

        if let Some(api_info) = api_info {
            Json(api_info).into_response()
        } else {
            StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    })
}

#[derive(Deserialize, IntoParams)]
pub struct LockParams {
    /// account name
    a: String,
}

#[utoipa::path(
    post,
    path = "/lock",
    tags = [ROOT],
    params(LockParams),
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "successfully locked"),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn lock(
    State(state): State<Arc<OrdinaryApiServerState>>,
    Query(query): Query<LockParams>,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("root");
    let span = span.in_scope(|| tracing::info_span!("lock"));

    span.in_scope(|| {
        match crate::server::check_ordinary_auth(&state, &headers, 1, "root") {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        match state.account_lock_manager.lock_account(query.a.as_str()) {
            Ok(()) => StatusCode::OK.into_response(),
            Err(err) => {
                tracing::error!(%err);
                StatusCode::INTERNAL_SERVER_ERROR.into_response()
            }
        }
    })
}

#[utoipa::path(
    post,
    path = "/unlock",
    tags = [ROOT],
    params(LockParams),
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "successfully unlocked"),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn unlock(
    State(state): State<Arc<OrdinaryApiServerState>>,
    Query(query): Query<LockParams>,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("root");
    let span = span.in_scope(|| tracing::info_span!("lock"));

    span.in_scope(|| {
        match crate::server::check_ordinary_auth(&state, &headers, 1, "root") {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        match state.account_lock_manager.unlock_account(query.a.as_str()) {
            Ok(()) => StatusCode::OK.into_response(),
            Err(err) => {
                tracing::error!(%err);
                StatusCode::INTERNAL_SERVER_ERROR.into_response()
            }
        }
    })
}