openobserve/src/main.rs

740 lines
25 KiB
Rust

// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// macOS ld cannot encode compact-unwind offsets once __eh_frame exceeds 16MB;
// harmless for a binary this size (only slows panic unwinding), so silence it.
#![allow(linker_messages)]
#[cfg(feature = "tokio-console")]
use std::net::SocketAddr;
use std::time::{Duration, SystemTime};
use common::{infra::cluster, meta};
use config::{
META_ORG_ID, get_config,
meta::triggers::{Trigger, TriggerModule, TriggerStatus},
utils::size::bytes_to_human_readable,
};
use db::{self, scheduler::TriggerModule::QueryRecommendations};
use infra::runtime::{create_grpc_runtime, create_job_runtime};
use openobserve::{
cli::basic::cli,
migration,
telemetry::{enable_tracing, setup_logs},
};
use openobserve_api_http::handler::http::router::*;
use openobserve_core::{bootstrap, metadata};
use openobserve_jobs::job;
use tokio::sync::oneshot;
#[cfg(feature = "enterprise")]
use tower_http::trace::TraceLayer;
use tracing_appender::non_blocking::WorkerGuard;
use utoipa::OpenApi;
#[cfg(feature = "enterprise")]
use {config::Config, o2_enterprise::enterprise::common::config::O2Config};
#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[cfg(feature = "jemalloc")]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[cfg(feature = "profiling")]
#[allow(non_upper_case_globals)]
#[unsafe(export_name = "malloc_conf")]
pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:16\0";
async fn flush_reporting() {
#[cfg(feature = "enterprise")]
audit::flush().await;
usage_reporting::flush().await;
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
// CLI provides the path to the config file (if any)
// In case a custom path is provided, the file will be read first
// and config variables will be loaded.
// This has to happen as the foremost step as any call to
// get_config without this would be loaded from local `.env`
// or environment itself.
if cli::cli().await? {
return Ok(());
}
#[cfg(feature = "tokio-console")]
console_subscriber::ConsoleLayer::builder()
.retention(Duration::from_secs(
get_config().tokio_console.tokio_console_retention,
))
.server_addr(
format!(
"{}:{}",
get_config().tokio_console.tokio_console_server_addr,
get_config().tokio_console.tokio_console_server_port
)
.as_str()
.parse::<SocketAddr>()?,
)
.init();
let cfg = get_config();
// setup logs
#[cfg(feature = "tokio-console")]
let enable_tokio_console = true;
#[cfg(not(feature = "tokio-console"))]
let enable_tokio_console = false;
let mut tracer_provider = None;
let _guard: Option<WorkerGuard> = if enable_tokio_console {
None
} else if cfg.common.should_create_span() {
log::info!("OpenTelemetry tracing enabled - initializing tracer provider");
tracer_provider = Some(enable_tracing()?);
log::info!("Tracer provider initialized successfully");
None
} else {
// Check if AI tracing is enabled independently
#[cfg(feature = "enterprise")]
{
use o2_enterprise::enterprise::common::config::get_config as get_o2_config;
let o2_cfg = get_o2_config();
if o2_cfg.ai.tracing_enabled {
tracer_provider = Some(enable_tracing()?);
None
} else {
Some(setup_logs())
}
}
#[cfg(not(feature = "enterprise"))]
{
Some(setup_logs())
}
};
log::info!("Starting OpenObserve {}", config::VERSION);
log::info!(
"System info: CPU cores {}, MEM total {}, Disk total {}, free {}",
cfg.limit.real_cpu_num,
bytes_to_human_readable(cfg.limit.mem_total as f64),
bytes_to_human_readable(cfg.limit.disk_total as f64),
bytes_to_human_readable(cfg.limit.disk_free as f64),
);
log::info!(
"Caches info: Disk max size {}, MEM max size {}, Datafusion pool size: {}",
bytes_to_human_readable((cfg.disk_cache.max_size * cfg.disk_cache.bucket_num) as f64),
bytes_to_human_readable((cfg.memory_cache.max_size * cfg.memory_cache.bucket_num) as f64),
bytes_to_human_readable(cfg.memory_cache.datafusion_max_size as f64),
);
// install ring as the default crypto provider if TLS is enabled
if cfg.http.tls_enabled || cfg.grpc.tls_enabled {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
}
// init action server
#[cfg(feature = "enterprise")]
if config::cluster::LOCAL_NODE.is_action_server() && config::cluster::LOCAL_NODE.is_standalone()
{
log::info!("Starting action server");
return init_action_server().await;
}
// init backend jobs
let (job_init_tx, job_init_rx) = oneshot::channel();
let (job_shutudown_tx, job_shutdown_rx) = oneshot::channel();
let (job_stopped_tx, job_stopped_rx) = oneshot::channel();
let job_rt_handle = std::thread::spawn(move || {
let Ok(rt) = create_job_runtime() else {
job_init_tx.send(false).ok();
panic!("job runtime init failed")
};
let _guard = rt.enter();
rt.block_on(async move {
// it must be initialized before the server starts
if let Err(e) = cluster::register_and_keep_alive().await {
job_init_tx.send(false).ok();
panic!("cluster init failed: {e}");
}
// init config
if let Err(e) = config::init().await {
job_init_tx.send(false).ok();
panic!("config init failed: {e}");
}
// db related inits
if let Err(e) = migration::init_db().await {
job_init_tx.send(false).ok();
panic!("db init failed: {e}");
}
// init infra
if let Err(e) = infra::init().await {
job_init_tx.send(false).ok();
panic!("infra init failed: {e}");
}
if let Err(e) = bootstrap::init().await {
job_init_tx.send(false).ok();
panic!("common infra init failed: {e}");
}
// Initialize MCP tools from the OpenAPI spec for all editions.
let api = openapi::ApiDoc::openapi();
if let Err(e) = openobserve_mcp::tools::init_mcp_tools(&api) {
log::error!("Failed to initialize MCP tools: {e}");
} else {
log::info!("Initialized MCP tools");
}
// init enterprise
#[cfg(feature = "enterprise")]
if let Err(e) = crate::init_enterprise().await {
job_init_tx.send(false).ok();
panic!("enterprise init failed: {e}");
}
// ingester init
if let Err(e) = ingester::init().await {
job_init_tx.send(false).ok();
panic!("ingester init failed: {e}");
}
// init job
if let Err(e) = job::init().await {
job_init_tx.send(false).ok();
panic!("job init failed: {e}");
}
// Register job runtime for metrics collection
if let Ok(handle) = tokio::runtime::Handle::try_current() {
openobserve_node::runtime_metrics::register_runtime("job".to_string(), handle);
}
job_init_tx.send(true).ok();
job_shutdown_rx.await.ok();
job_stopped_tx.send(()).ok();
// flush distinct values
_ = metadata::close().await;
// flush WAL cache to disk
_ = ingester::flush_all().await;
// flush compact offset cache to disk disk
_ = db::compact::files::sync_cache_to_db().await;
// flush db
let db = infra::db::get_db().await;
_ = db.close().await;
});
});
// wait for job init
match job_init_rx.await {
Ok(true) => log::info!("backend job init success"),
Ok(false) => {
return Err(anyhow::anyhow!("backend job init failed, exiting"));
}
Err(e) => {
return Err(anyhow::anyhow!("backend job init failed: {}", e));
}
}
// init gRPC server
let (grpc_init_tx, grpc_init_rx) = oneshot::channel();
let (grpc_shutudown_tx, grpc_shutdown_rx) = oneshot::channel();
let (grpc_stopped_tx, grpc_stopped_rx) = oneshot::channel();
let grpc_rt_handle = std::thread::spawn(move || {
let Ok(rt) = create_grpc_runtime() else {
grpc_init_tx.send(()).ok();
panic!("grpc runtime init failed");
};
// Register gRPC runtime for metrics collection
openobserve_node::runtime_metrics::register_runtime(
"grpc".to_string(),
rt.handle().clone(),
);
let _guard = rt.enter();
rt.block_on(async move {
let ret =
openobserve_api_grpc::server::run(grpc_init_tx, grpc_shutdown_rx, grpc_stopped_tx)
.await;
if let Err(e) = ret {
log::error!("gRPC server init failed: {e}");
std::process::exit(1);
}
});
});
// wait for gRPC init
grpc_init_rx.await.ok();
// Register main HTTP runtime for metrics collection
if let Ok(handle) = tokio::runtime::Handle::try_current() {
openobserve_node::runtime_metrics::register_runtime("http".to_string(), handle);
}
// Start runtime metrics collector
openobserve_node::runtime_metrics::start_metrics_collector().await;
// let node online
let _ = cluster::set_online().await;
// initialize the jobs are deferred until the gRPC service starts
job::init_deferred()
.await
.expect("Deferred jobs failed to init");
if cfg.common.telemetry_enabled {
tokio::task::spawn(async move {
meta::telemetry::Telemetry::new()
.send_track_event("OpenObserve - Starting server", None, true, false)
.await;
});
}
// let node schedulable
let mut start_ok = false;
for _ in 0..10 {
match cluster::set_schedulable().await {
Ok(_) => {
start_ok = true;
break;
}
Err(e) => {
log::error!("set node schedulable failed: {e}");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
if !start_ok {
return Err(anyhow::anyhow!("set node schedulable failed"));
}
// Check for query recommendations trigger and create one
match db::scheduler::list(Some(QueryRecommendations)).await {
Ok(list) if list.len() == 1 => {}
_ => {
let _ = db::scheduler::delete(
META_ORG_ID,
TriggerModule::QueryRecommendations,
"QueryRecommendations",
)
.await
.inspect_err(|e| {
log::error!("Error while purging recommendations triggers. e={:?}", e);
});
let now = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards")
.as_micros() as i64;
// Get the next minute on the clock (e.g., if now is 10:37:25am, next_minute is
// 10:38:00am)
let minute_micros = 60 * 1_000_000; // 60 seconds in microseconds
let next_minute = (now / minute_micros + 1) * minute_micros;
let trigger = Trigger {
org: META_ORG_ID.to_string(),
module: TriggerModule::QueryRecommendations,
module_key: "QueryRecommendations".to_string(),
next_run_at: next_minute,
status: TriggerStatus::Waiting,
start_time: Some(next_minute),
end_time: None,
retries: 3,
..Default::default()
};
let _ = db::scheduler::push(trigger).await.inspect_err(|e| {
log::error!(
"Failed to setup the initial trigger for recommendations. e={:?}",
e
)
});
log::info!("[QUERY_RECOMMENDATIONS] Setup the initial trigger.");
}
};
// init http server
if let Err(e) = openobserve_api_http::server::run(web::ui_routes).await {
log::error!("HTTP server runs failed: {e}");
}
log::info!("HTTP server stopped");
// stop tracing
if let Some(tracer_provider) = tracer_provider {
let result = tracer_provider.shutdown();
log::info!("Tracer provider shutdown result: {result:?}");
}
// flush usage report
flush_reporting().await;
// flush service discovery
#[cfg(feature = "enterprise")]
{
log::info!("Flushing service discovery...");
if let Err(e) =
o2_enterprise::enterprise::service_streams::batch_processor::flush_all().await
{
log::error!("Failed to flush service discovery: {}", e);
}
log::info!("Flushing Gen-AI agent registry...");
if let Err(e) =
o2_enterprise::enterprise::llm_evaluations::agent_registry::flush_all().await
{
log::error!("Failed to flush Gen-AI agent registry: {}", e);
}
}
// leave the cluster
_ = cluster::leave().await;
log::info!("Node left cluster");
// stop gRPC server
grpc_shutudown_tx.send(()).ok();
grpc_stopped_rx.await.ok();
grpc_rt_handle.join().ok();
log::info!("gRPC server stopped");
// stop backend jobs
job_shutudown_tx.send(()).ok();
job_stopped_rx.await.ok();
job_rt_handle.join().ok();
log::info!("backend job stopped");
// stop telemetry
if cfg.common.telemetry_enabled {
meta::telemetry::Telemetry::new()
.send_track_event("OpenObserve - Server stopped", None, true, true)
.await;
}
log::info!("server stopped");
Ok(())
}
#[cfg(feature = "enterprise")]
async fn init_action_server() -> Result<(), anyhow::Error> {
let cfg = get_config();
let haddr = openobserve_api_http::server::server_addr()?;
// Setup the namespace
o2_enterprise::enterprise::actions::action_deployer::init().await?;
log::info!(
"Starting Action Server {} server at: {haddr}",
if cfg.http.tls_enabled {
"HTTPS"
} else {
"HTTP"
}
);
// Build the router for action server
let app = openobserve_api_http::server::apply_common_middlewares(create_action_server_router())
.layer(TraceLayer::new_for_http());
openobserve_api_http::server::serve(haddr, app).await?;
log::info!("HTTP server stopped");
// flush usage report
flush_reporting().await;
// stop telemetry
if cfg.common.telemetry_enabled {
meta::telemetry::Telemetry::new()
.send_track_event("OpenObserve - Server stopped", None, true, true)
.await;
}
log::info!("server stopped");
Ok(())
}
#[cfg(feature = "enterprise")]
pub fn create_action_server_router() -> axum::Router {
use axum::{
Router,
extract::DefaultBodyLimit,
middleware,
routing::{get, post},
};
use openobserve_api_http::handler::http::router::cors_layer;
use openobserve_api_management::request::action_server;
let cfg = get_config();
// Create action server routes with authentication
// Routes match action_manager.rs expected URLs: /api/{org_id}/v1/job[/{id}]
let api_routes = Router::new()
.route(
"/{org_id}/v1/job",
post(action_server::create_job).get(action_server::list_deployed_apps),
)
.route(
"/{org_id}/v1/job/{name}",
get(action_server::get_app_details)
.delete(action_server::delete_job)
.put(action_server::patch_action),
)
.layer(middleware::from_fn(
openobserve_api_common::auth::action_server::auth_middleware,
))
.layer(cors_layer());
// Nest under base URI and set request body size limit
let router = Router::new().nest(&format!("{}/api", cfg.common.base_uri), api_routes);
// Set request body size limit (equivalent to actix-web's PayloadConfig)
router.layer(DefaultBodyLimit::max(cfg.limit.req_payload_limit))
}
/// Initializes enterprise features.
#[cfg(feature = "enterprise")]
async fn init_enterprise() -> Result<(), anyhow::Error> {
o2_enterprise::enterprise::search::init().await?;
if let Err(e) = o2_enterprise::enterprise::actions::action_manager::init_client() {
log::warn!("Failed to init action manager client: {e}");
}
if o2_enterprise::enterprise::common::config::get_config()
.super_cluster
.enabled
{
log::info!("init super cluster");
o2_enterprise::enterprise::super_cluster::kv::init().await?;
super_cluster_queue::init().await?;
}
// Initialize enterprise AI components (agent and evaluation clients).
if let Err(e) = o2_enterprise::enterprise::ai::init_ai_components() {
log::error!("Failed to initialize enterprise AI components: {e}");
} else {
log::info!("Initialized enterprise AI components");
}
// check ratelimit config
let cfg = config::get_config();
let o2cfg = o2_enterprise::enterprise::common::config::get_config();
if let Err(e) = check_ratelimit_config(&cfg, &o2cfg) {
panic!("ratelimit config error: {e}");
}
// Push the synthetics limits from enterprise config into the OSS validator.
// Synthetics is enterprise-only, so the values live in SyntheticsConfig, but
// the check validation they bound lives in `config` — which cannot depend on
// o2_enterprise. This is the seam.
//
// Deliberately NOT fatal, unlike ratelimit above: synthetics is one feature,
// and refusing to start the whole application — ingest, search, dashboards —
// because a probe ceiling is misconfigured would be the worse outage. On
// rejection nothing is installed and validation keeps using the conservative
// built-in defaults, so the failure mode is "stricter than intended", not
// "accepts checks that get killed mid-run".
if let Err(e) =
config::meta::synthetics::init_limits(config::meta::synthetics::SyntheticsLimits {
job_lease_secs: o2cfg.synthetics.job_lease_secs,
max_check_budget_secs: o2cfg.synthetics.max_check_budget_secs,
max_net_timeout_ms: o2cfg.synthetics.max_net_timeout_ms,
})
{
log::error!(
"synthetics limits config rejected, falling back to defaults \
(budget={}s lease={}s): {e}",
config::meta::synthetics::DEFAULT_MAX_CHECK_BUDGET_SECS,
config::meta::synthetics::DEFAULT_JOB_LEASE_SECS,
);
}
o2_enterprise::enterprise::pipeline::pipeline_file_server::PipelineFileServer::run().await?;
if o2cfg.rate_limit.rate_limit_enabled && o2_openfga::config::get_config().enabled {
o2_ratelimit::init(
openobserve_api_http::handler::http::router::openapi::openapi_info().await,
)
.await?;
}
Ok(())
}
#[cfg(feature = "enterprise")]
fn check_ratelimit_config(cfg: &Config, o2cfg: &O2Config) -> Result<(), anyhow::Error> {
if o2cfg.rate_limit.rate_limit_enabled {
let queue_store =
config::meta::queue_store::QueueStore::try_from(cfg.common.queue_store.as_str());
if queue_store != Ok(config::meta::queue_store::QueueStore::Nats) {
return Err(anyhow::anyhow!(
"ZO_QUEUE_STORE must be nats when ratelimit is enabled"
));
}
}
if o2cfg.rate_limit.rate_limit_rule_refresh_interval < 2 {
return Err(anyhow::anyhow!(
"ratelimit rules refresh interval must be greater than or equal to 2 seconds"
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "enterprise")]
#[test]
#[ignore] // TODO: Fix enterprise config structure issues
fn test_check_ratelimit_config_valid() {
// Test disabled due to enterprise config structure mismatch
// Need to properly construct O2Config and RateLimitConfig structs
}
#[cfg(feature = "enterprise")]
#[test]
#[ignore = "Enterprise config struct fields don't match - needs fixing"]
fn test_check_ratelimit_config_invalid_interval() {
// Test disabled due to enterprise config structure mismatch
// Need to properly construct O2Config and RateLimitConfig structs
}
#[tokio::test]
async fn test_socket_addr_parsing() {
use std::net::SocketAddr;
// Test IPv4 socket address parsing (used in init_common_grpc_server)
let addr: Result<SocketAddr, _> = "127.0.0.1:8080".parse();
assert!(addr.is_ok());
// Test IPv6 socket address parsing (used in HTTP server)
let addr: Result<SocketAddr, _> = "[::]:8080".parse();
assert!(addr.is_ok());
// Test invalid address
let addr: Result<SocketAddr, _> = "invalid:address".parse();
assert!(addr.is_err());
}
#[tokio::test]
async fn test_oneshot_channel_communication() {
use tokio::sync::oneshot;
// Test the oneshot channel pattern used for server coordination
let (tx, rx) = oneshot::channel::<bool>();
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
// Simulate successful initialization
let handle = tokio::spawn(async move {
// Simulate some async work
tokio::time::sleep(Duration::from_millis(10)).await;
tx.send(true).unwrap();
// Wait for shutdown signal
shutdown_rx.await.unwrap();
"completed"
});
// Wait for initialization
let result = rx.await;
assert!(result.is_ok());
assert!(result.unwrap());
// Send shutdown signal
shutdown_tx.send(()).unwrap();
// Wait for completion
let result = handle.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "completed");
}
#[test]
fn test_server_configuration_limits() {
// Test the limit calculations used in server setup
let http_worker_num = 4;
let http_worker_max_blocking = 8;
// Pattern used in script server configuration
let total_blocking = http_worker_num * http_worker_max_blocking;
assert_eq!(total_blocking, 32);
// Test bounds checking
let keep_alive = std::cmp::max(1, 30);
assert!(keep_alive >= 1);
let timeout = std::cmp::max(1, 60);
assert!(timeout >= 1);
}
#[tokio::test]
async fn test_telemetry_event_creation() {
// Test telemetry event patterns used in the main function
let event_name = "OpenObserve - Starting server";
let stop_event = "OpenObserve - Server stopped";
assert!(event_name.contains("OpenObserve"));
assert!(event_name.contains("Starting"));
assert!(stop_event.contains("Server stopped"));
// Test boolean flags used in telemetry calls
let server_start = true;
let wait_for_send = false;
let server_stop = true;
let wait_for_stop = true;
assert!(server_start);
assert!(!wait_for_send);
assert!(server_stop);
assert!(wait_for_stop);
}
#[tokio::test]
async fn test_signal_handling_patterns() {
use tokio::sync::oneshot;
// Test the select pattern used in graceful shutdown
let (tx1, mut rx1) = oneshot::channel::<&str>();
let (tx2, mut rx2) = oneshot::channel::<&str>();
// Simulate signal reception
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
tx1.send("SIGTERM received").unwrap();
});
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
tx2.send("SIGINT received").unwrap();
});
// Test select! pattern
let result = tokio::select! {
msg = &mut rx1 => msg.unwrap(),
msg = &mut rx2 => msg.unwrap(),
};
assert!(result.contains("SIG"));
assert!(result.contains("received"));
}
}