mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
1598 lines
57 KiB
Rust
1598 lines
57 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/>.
|
|
|
|
use std::{
|
|
cmp::max,
|
|
collections::HashMap,
|
|
net::SocketAddr,
|
|
str::FromStr,
|
|
time::{Duration, SystemTime},
|
|
};
|
|
|
|
use arrow_flight::flight_service_server::FlightServiceServer;
|
|
use config::{
|
|
META_ORG_ID, get_config,
|
|
meta::triggers::{Trigger, TriggerModule, TriggerStatus},
|
|
utils::size::bytes_to_human_readable,
|
|
};
|
|
use log::LevelFilter;
|
|
use openobserve::{
|
|
cli::basic::cli,
|
|
common::{
|
|
infra::{self as common_infra, cluster},
|
|
meta,
|
|
utils::zo_logger,
|
|
},
|
|
handler::{
|
|
grpc::{
|
|
auth::check_auth,
|
|
flight::FlightServiceImpl,
|
|
request::{
|
|
event::Eventer,
|
|
ingest::Ingester,
|
|
logs::LogsServer,
|
|
metrics::{ingester::MetricsIngester, querier::MetricsQuerier},
|
|
query_cache::QueryCacheServerImpl,
|
|
stream::StreamServiceImpl,
|
|
traces::TraceServer,
|
|
},
|
|
},
|
|
http::router::*,
|
|
},
|
|
job, migration, router,
|
|
service::{
|
|
cluster_info::ClusterInfoService,
|
|
db::{self, scheduler::TriggerModule::QueryRecommendations},
|
|
metadata,
|
|
node::NodeService,
|
|
search::SEARCH_SERVER,
|
|
self_reporting,
|
|
},
|
|
};
|
|
use opentelemetry::{KeyValue, global, trace::TracerProvider};
|
|
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, WithTonicConfig};
|
|
use opentelemetry_proto::tonic::collector::{
|
|
logs::v1::logs_service_server::LogsServiceServer,
|
|
metrics::v1::metrics_service_server::MetricsServiceServer,
|
|
trace::v1::trace_service_server::TraceServiceServer,
|
|
};
|
|
use opentelemetry_sdk::{Resource, propagation::TraceContextPropagator};
|
|
use proto::cluster_rpc::{
|
|
cluster_info_service_server::ClusterInfoServiceServer, event_server::EventServer,
|
|
ingest_server::IngestServer, metrics_server::MetricsServer,
|
|
node_service_server::NodeServiceServer, query_cache_server::QueryCacheServer,
|
|
search_server::SearchServer, streams_server::StreamsServer,
|
|
};
|
|
#[cfg(feature = "pyroscope")]
|
|
use pyroscope::PyroscopeAgent;
|
|
#[cfg(feature = "pyroscope")]
|
|
use pyroscope_pprofrs::{PprofConfig, pprof_backend};
|
|
use tokio::{net::TcpListener, sync::oneshot};
|
|
use tonic::{
|
|
codec::CompressionEncoding,
|
|
metadata::{MetadataKey, MetadataMap, MetadataValue},
|
|
transport::{Identity, ServerTlsConfig},
|
|
};
|
|
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
|
use tracing_appender::non_blocking::WorkerGuard;
|
|
use tracing_opentelemetry::OpenTelemetryLayer;
|
|
use tracing_subscriber::Registry;
|
|
#[cfg(feature = "enterprise")]
|
|
use {
|
|
config::Config,
|
|
o2_enterprise::enterprise::{ai, common::config::O2Config},
|
|
utoipa::OpenApi,
|
|
};
|
|
|
|
#[cfg(feature = "mimalloc")]
|
|
#[global_allocator]
|
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
#[cfg(feature = "jemalloc")]
|
|
#[global_allocator]
|
|
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
|
use tracing_subscriber::{
|
|
EnvFilter, filter::LevelFilter as TracingLevelFilter, fmt::Layer, prelude::*,
|
|
};
|
|
|
|
#[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";
|
|
|
|
#[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();
|
|
|
|
// setup pyroscope
|
|
#[cfg(feature = "pyroscope")]
|
|
let pyroscope_agent = if get_config().profiling.pyroscope_enabled {
|
|
let agent = PyroscopeAgent::builder(
|
|
&get_config().profiling.pyroscope_server_url,
|
|
&get_config().profiling.pyroscope_project_name,
|
|
)
|
|
.tags(
|
|
[
|
|
("role", get_config().common.node_role.as_str()),
|
|
("instance", get_config().common.instance_name.as_str()),
|
|
("version", config::VERSION),
|
|
]
|
|
.to_vec(),
|
|
)
|
|
.backend(pprof_backend(PprofConfig::new().sample_rate(100)))
|
|
.build()
|
|
.expect("Failed to setup pyroscope agent");
|
|
let agent_running = agent.start().expect("Failed to start pyroscope agent");
|
|
Some(agent_running)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
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.log.events_enabled {
|
|
let logger = zo_logger::ZoLogger {
|
|
sender: zo_logger::EVENT_SENDER.clone(),
|
|
};
|
|
log::set_boxed_logger(Box::new(logger)).map(|()| {
|
|
log::set_max_level(LevelFilter::from_str(&cfg.log.level).unwrap_or(LevelFilter::Info))
|
|
})?;
|
|
None
|
|
} else if cfg.common.tracing_enabled || cfg.common.tracing_search_enabled {
|
|
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 cfg = get_config();
|
|
let Ok(rt) = tokio::runtime::Builder::new_multi_thread()
|
|
.worker_threads(cfg.limit.job_runtime_worker_num)
|
|
.enable_all()
|
|
.thread_name("job_runtime")
|
|
.max_blocking_threads(cfg.limit.job_runtime_blocking_worker_num)
|
|
.build()
|
|
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) = common_infra::init().await {
|
|
job_init_tx.send(false).ok();
|
|
panic!("common infra init failed: {e}");
|
|
}
|
|
|
|
// 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::service::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 cfg = get_config();
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.worker_threads(cfg.limit.grpc_runtime_worker_num)
|
|
.enable_all()
|
|
.thread_name("grpc_runtime")
|
|
.max_blocking_threads(cfg.limit.grpc_runtime_blocking_worker_num)
|
|
.build()
|
|
.expect("grpc runtime init failed");
|
|
|
|
// Register gRPC runtime for metrics collection
|
|
openobserve::service::runtime_metrics::register_runtime(
|
|
"grpc".to_string(),
|
|
rt.handle().clone(),
|
|
);
|
|
|
|
let _guard = rt.enter();
|
|
rt.block_on(async move {
|
|
let ret = if config::cluster::LOCAL_NODE.is_router() {
|
|
init_router_grpc_server(grpc_init_tx, grpc_shutdown_rx, grpc_stopped_tx).await
|
|
} else {
|
|
init_common_grpc_server(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::service::runtime_metrics::register_runtime("http".to_string(), handle);
|
|
}
|
|
|
|
// Start runtime metrics collector
|
|
openobserve::service::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.log.events_enabled {
|
|
tokio::task::spawn(zo_logger::send_logs());
|
|
}
|
|
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 !cfg.common.tracing_enabled && cfg.common.tracing_search_enabled {
|
|
if let Err(e) = init_http_server_without_tracing().await {
|
|
log::error!("HTTP server runs failed: {e}");
|
|
}
|
|
} else if let Err(e) = init_http_server().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
|
|
self_reporting::flush().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);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// stop pyroscope
|
|
#[cfg(feature = "pyroscope")]
|
|
if let Some(agent) = pyroscope_agent
|
|
&& let Ok(agent_ready) = agent.stop()
|
|
{
|
|
agent_ready.shutdown();
|
|
}
|
|
|
|
log::info!("server stopped");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn init_common_grpc_server(
|
|
init_tx: oneshot::Sender<()>,
|
|
shutdown_rx: oneshot::Receiver<()>,
|
|
stopped_tx: oneshot::Sender<()>,
|
|
) -> Result<(), anyhow::Error> {
|
|
let cfg = get_config();
|
|
let ip = if !cfg.grpc.addr.is_empty() {
|
|
cfg.grpc.addr.clone()
|
|
} else {
|
|
"0.0.0.0".to_string()
|
|
};
|
|
let gaddr: SocketAddr = format!("{}:{}", ip, cfg.grpc.port).parse()?;
|
|
let event_svc = EventServer::new(Eventer)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let search_svc = SearchServer::new(SEARCH_SERVER.clone())
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let metrics_svc = MetricsServer::new(MetricsQuerier)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let metrics_ingest_svc = MetricsServiceServer::new(MetricsIngester)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let logs_svc = LogsServiceServer::new(LogsServer)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let trace_svc = TraceServiceServer::new(TraceServer)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let query_cache_svc = QueryCacheServer::new(QueryCacheServerImpl)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let ingest_svc = IngestServer::new(Ingester)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let streams_svc = StreamsServer::new(StreamServiceImpl)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let flight_svc = FlightServiceServer::new(FlightServiceImpl)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let node_svc = NodeServiceServer::new(NodeService)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let cluster_info_svc = ClusterInfoServiceServer::new(ClusterInfoService)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
|
|
log::info!(
|
|
"starting gRPC server {} at {}",
|
|
if cfg.grpc.tls_enabled { "with TLS" } else { "" },
|
|
gaddr
|
|
);
|
|
init_tx.send(()).ok();
|
|
|
|
let builder = if cfg.grpc.tls_enabled {
|
|
let cert = std::fs::read_to_string(&cfg.grpc.tls_cert_path)?;
|
|
let key = std::fs::read_to_string(&cfg.grpc.tls_key_path)?;
|
|
let identity = Identity::from_pem(cert, key);
|
|
tonic::transport::Server::builder().tls_config(ServerTlsConfig::new().identity(identity))?
|
|
} else {
|
|
tonic::transport::Server::builder()
|
|
};
|
|
let ret = builder
|
|
.layer(tonic::service::InterceptorLayer::new(check_auth))
|
|
.add_service(event_svc)
|
|
.add_service(search_svc)
|
|
.add_service(metrics_svc)
|
|
.add_service(metrics_ingest_svc)
|
|
.add_service(trace_svc)
|
|
.add_service(logs_svc)
|
|
.add_service(query_cache_svc)
|
|
.add_service(ingest_svc)
|
|
.add_service(streams_svc)
|
|
.add_service(flight_svc)
|
|
.add_service(node_svc)
|
|
.add_service(cluster_info_svc)
|
|
.serve_with_shutdown(gaddr, async {
|
|
shutdown_rx.await.ok();
|
|
log::info!("gRPC server starts shutting down");
|
|
})
|
|
.await;
|
|
if let Err(e) = ret {
|
|
return Err(anyhow::anyhow!("{e}"));
|
|
}
|
|
|
|
stopped_tx.send(()).ok();
|
|
Ok(())
|
|
}
|
|
|
|
async fn init_router_grpc_server(
|
|
init_tx: oneshot::Sender<()>,
|
|
shutdown_rx: oneshot::Receiver<()>,
|
|
stopped_tx: oneshot::Sender<()>,
|
|
) -> Result<(), anyhow::Error> {
|
|
let cfg = get_config();
|
|
let gaddr: SocketAddr = format!("0.0.0.0:{}", cfg.grpc.port).parse()?;
|
|
let logs_svc = LogsServiceServer::new(router::grpc::ingest::logs::LogsServer)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let metrics_svc = MetricsServiceServer::new(router::grpc::ingest::metrics::MetricsServer)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
let traces_svc = TraceServiceServer::new(router::grpc::ingest::traces::TraceServer)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.max_decoding_message_size(cfg.grpc.max_message_size * 1024 * 1024)
|
|
.max_encoding_message_size(cfg.grpc.max_message_size * 1024 * 1024);
|
|
|
|
log::info!(
|
|
"starting gRPC server {} at {}",
|
|
if cfg.grpc.tls_enabled { "with TLS" } else { "" },
|
|
gaddr
|
|
);
|
|
init_tx.send(()).ok();
|
|
|
|
let builder = if cfg.grpc.tls_enabled {
|
|
let cert = std::fs::read_to_string(&cfg.grpc.tls_cert_path)?;
|
|
let key = std::fs::read_to_string(&cfg.grpc.tls_key_path)?;
|
|
let identity = Identity::from_pem(cert, key);
|
|
tonic::transport::Server::builder().tls_config(ServerTlsConfig::new().identity(identity))?
|
|
} else {
|
|
tonic::transport::Server::builder()
|
|
};
|
|
let ret = builder
|
|
.layer(tonic::service::InterceptorLayer::new(check_auth))
|
|
.add_service(logs_svc)
|
|
.add_service(metrics_svc)
|
|
.add_service(traces_svc)
|
|
.serve_with_shutdown(gaddr, async {
|
|
shutdown_rx.await.ok();
|
|
log::info!("gRPC server starts shutting down");
|
|
})
|
|
.await;
|
|
if let Err(e) = ret {
|
|
return Err(anyhow::anyhow!("{e}"));
|
|
}
|
|
|
|
stopped_tx.send(()).ok();
|
|
Ok(())
|
|
}
|
|
|
|
async fn init_http_server() -> Result<(), anyhow::Error> {
|
|
let cfg = get_config();
|
|
|
|
let haddr: SocketAddr = if cfg.http.ipv6_enabled {
|
|
format!("[::]:{}", cfg.http.port).parse()?
|
|
} else {
|
|
let ip = if !cfg.http.addr.is_empty() {
|
|
cfg.http.addr.clone()
|
|
} else {
|
|
"0.0.0.0".to_string()
|
|
};
|
|
format!("{}:{}", ip, cfg.http.port).parse()?
|
|
};
|
|
|
|
let scheme = if cfg.http.tls_enabled {
|
|
"HTTPS"
|
|
} else {
|
|
"HTTP"
|
|
};
|
|
log::info!("Starting {scheme} server at: {haddr}");
|
|
|
|
// Build the router
|
|
let app = create_app_router()
|
|
.layer(config::axum::middlewares::AccessLogLayer::new(
|
|
config::axum::middlewares::get_http_access_log_format(),
|
|
))
|
|
.layer(config::axum::middlewares::SlowLogLayer::new(
|
|
cfg.limit.http_slow_log_threshold,
|
|
))
|
|
.layer(CompressionLayer::new())
|
|
.layer(TraceLayer::new_for_http());
|
|
|
|
if cfg.http.tls_enabled {
|
|
// TLS server using axum-server
|
|
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(
|
|
&cfg.http.tls_cert_path,
|
|
&cfg.http.tls_key_path,
|
|
)
|
|
.await?;
|
|
|
|
let handle = axum_server::Handle::new();
|
|
let shutdown_timeout = cfg.limit.http_shutdown_timeout;
|
|
|
|
// Spawn task to handle shutdown signal
|
|
tokio::spawn({
|
|
let handle = handle.clone();
|
|
async move {
|
|
shutdown_signal().await;
|
|
handle
|
|
.graceful_shutdown(Some(Duration::from_secs(max(1, shutdown_timeout as u64))));
|
|
}
|
|
});
|
|
|
|
axum_server::bind_rustls(haddr, tls_config)
|
|
.handle(handle)
|
|
.serve(app.into_make_service())
|
|
.await?;
|
|
} else {
|
|
// Non-TLS server
|
|
let listener = TcpListener::bind(haddr).await?;
|
|
axum::serve(listener, app.into_make_service())
|
|
.with_graceful_shutdown(shutdown_signal())
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn init_http_server_without_tracing() -> Result<(), anyhow::Error> {
|
|
let cfg = get_config();
|
|
|
|
let haddr: SocketAddr = if cfg.http.ipv6_enabled {
|
|
format!("[::]:{}", cfg.http.port).parse()?
|
|
} else {
|
|
let ip = if !cfg.http.addr.is_empty() {
|
|
cfg.http.addr.clone()
|
|
} else {
|
|
"0.0.0.0".to_string()
|
|
};
|
|
format!("{}:{}", ip, cfg.http.port).parse()?
|
|
};
|
|
|
|
let scheme = if cfg.http.tls_enabled {
|
|
"HTTPS"
|
|
} else {
|
|
"HTTP"
|
|
};
|
|
log::info!("Starting {scheme} server at: {haddr}");
|
|
|
|
// Build the router without tracing
|
|
let app = create_app_router()
|
|
.layer(config::axum::middlewares::SlowLogLayer::new(
|
|
cfg.limit.http_slow_log_threshold,
|
|
))
|
|
.layer(CompressionLayer::new());
|
|
|
|
if cfg.http.tls_enabled {
|
|
// TLS server using axum-server
|
|
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(
|
|
&cfg.http.tls_cert_path,
|
|
&cfg.http.tls_key_path,
|
|
)
|
|
.await?;
|
|
|
|
let handle = axum_server::Handle::new();
|
|
let shutdown_timeout = cfg.limit.http_shutdown_timeout;
|
|
|
|
// Spawn task to handle shutdown signal
|
|
tokio::spawn({
|
|
let handle = handle.clone();
|
|
async move {
|
|
shutdown_signal().await;
|
|
handle
|
|
.graceful_shutdown(Some(Duration::from_secs(max(1, shutdown_timeout as u64))));
|
|
}
|
|
});
|
|
|
|
axum_server::bind_rustls(haddr, tls_config)
|
|
.handle(handle)
|
|
.serve(app.into_make_service())
|
|
.await?;
|
|
} else {
|
|
// Non-TLS server
|
|
let listener = TcpListener::bind(haddr).await?;
|
|
axum::serve(listener, app.into_make_service())
|
|
.with_graceful_shutdown(shutdown_signal())
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Signal handler for graceful shutdown
|
|
async fn shutdown_signal() {
|
|
#[cfg(unix)]
|
|
{
|
|
use tokio::signal::unix::{SignalKind, signal};
|
|
|
|
let mut sigquit = signal(SignalKind::quit()).unwrap();
|
|
let mut sigterm = signal(SignalKind::terminate()).unwrap();
|
|
let mut sigint = signal(SignalKind::interrupt()).unwrap();
|
|
|
|
tokio::select! {
|
|
_ = sigquit.recv() => log::info!("SIGQUIT received"),
|
|
_ = sigterm.recv() => log::info!("SIGTERM received"),
|
|
_ = sigint.recv() => log::info!("SIGINT received"),
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
{
|
|
use tokio::signal::windows::*;
|
|
|
|
let mut sigbreak = ctrl_break().unwrap();
|
|
let mut sigint = ctrl_c().unwrap();
|
|
let mut sigquit = ctrl_close().unwrap();
|
|
let mut sigterm = ctrl_shutdown().unwrap();
|
|
|
|
tokio::select! {
|
|
_ = sigbreak.recv() => log::info!("ctrl-break received"),
|
|
_ = sigquit.recv() => log::info!("ctrl-c received"),
|
|
_ = sigterm.recv() => log::info!("ctrl-close received"),
|
|
_ = sigint.recv() => log::info!("ctrl-shutdown received"),
|
|
}
|
|
}
|
|
|
|
// offline the node
|
|
if let Err(e) = cluster::set_offline().await {
|
|
log::error!("set offline failed: {e}");
|
|
}
|
|
log::info!("Node is offline");
|
|
}
|
|
|
|
/// Setup the tracing related components
|
|
pub(crate) fn setup_logs() -> tracing_appender::non_blocking::WorkerGuard {
|
|
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
|
|
|
let cfg = get_config();
|
|
let (writer, guard) = if cfg.log.file_dir.is_empty() {
|
|
let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout());
|
|
(BoxMakeWriter::new(non_blocking), _guard)
|
|
} else {
|
|
let file_name_prefix = if cfg.log.file_name_prefix.is_empty() {
|
|
format!("o2.{}.log", cfg.common.instance_name.as_str())
|
|
} else {
|
|
cfg.log.file_name_prefix.to_string()
|
|
};
|
|
let file_appender = tracing_appender::rolling::daily(&cfg.log.file_dir, file_name_prefix);
|
|
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
|
(BoxMakeWriter::new(non_blocking), _guard)
|
|
};
|
|
let layer = if cfg.log.json_format {
|
|
Layer::default()
|
|
.with_writer(writer)
|
|
.with_timer(config::meta::logger::CustomTimeFormat)
|
|
.with_ansi(false)
|
|
.json()
|
|
.with_current_span(false)
|
|
.with_span_list(false)
|
|
.boxed()
|
|
} else {
|
|
Layer::default()
|
|
.with_writer(writer)
|
|
.with_ansi(false)
|
|
.with_target(true)
|
|
.event_format(config::meta::logger::O2Formatter::default())
|
|
.boxed()
|
|
};
|
|
|
|
tracing_subscriber::registry()
|
|
.with(
|
|
EnvFilter::builder()
|
|
.with_default_directive(TracingLevelFilter::INFO.into())
|
|
.from_env_lossy(),
|
|
)
|
|
.with(layer)
|
|
.init();
|
|
guard
|
|
}
|
|
|
|
/// Custom span processor that filters spans based on their name prefix
|
|
#[cfg(feature = "enterprise")]
|
|
#[derive(Debug)]
|
|
struct FilteringSpanProcessor<P> {
|
|
inner: P,
|
|
prefix_filter: Option<String>,
|
|
}
|
|
|
|
#[cfg(feature = "enterprise")]
|
|
impl<P> FilteringSpanProcessor<P> {
|
|
fn new(inner: P, prefix_filter: Option<String>) -> Self {
|
|
Self {
|
|
inner,
|
|
prefix_filter,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "enterprise")]
|
|
impl<P: opentelemetry_sdk::trace::SpanProcessor> opentelemetry_sdk::trace::SpanProcessor
|
|
for FilteringSpanProcessor<P>
|
|
{
|
|
fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) {
|
|
// Always call inner on_start - we can only filter on_end when we have the full span data
|
|
self.inner.on_start(span, cx);
|
|
}
|
|
|
|
fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {
|
|
// If no filter is set, pass through all spans
|
|
if let Some(prefix) = &self.prefix_filter {
|
|
let span_name = span.name.as_ref();
|
|
// Only process spans that match the prefix
|
|
if !span_name.starts_with(prefix) {
|
|
return;
|
|
}
|
|
}
|
|
self.inner.on_end(span);
|
|
}
|
|
|
|
fn force_flush(&self) -> Result<(), opentelemetry_sdk::error::OTelSdkError> {
|
|
self.inner.force_flush()
|
|
}
|
|
|
|
fn shutdown(&self) -> Result<(), opentelemetry_sdk::error::OTelSdkError> {
|
|
self.inner.shutdown()
|
|
}
|
|
|
|
fn shutdown_with_timeout(
|
|
&self,
|
|
timeout: std::time::Duration,
|
|
) -> Result<(), opentelemetry_sdk::error::OTelSdkError> {
|
|
self.inner.shutdown_with_timeout(timeout)
|
|
}
|
|
}
|
|
|
|
fn enable_tracing() -> Result<opentelemetry_sdk::trace::SdkTracerProvider, anyhow::Error> {
|
|
let cfg = get_config();
|
|
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
|
|
|
|
let mut tracer_builder = opentelemetry_sdk::trace::SdkTracerProvider::builder();
|
|
|
|
// Add main OpenObserve OTLP exporter (if general tracing is enabled)
|
|
if cfg.common.tracing_enabled || cfg.common.tracing_search_enabled {
|
|
tracer_builder = if cfg.common.otel_otlp_grpc_url.is_empty() {
|
|
tracer_builder.with_span_processor(
|
|
opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor::builder(
|
|
{
|
|
let mut headers = HashMap::new();
|
|
headers.insert(
|
|
cfg.common.tracing_header_key.clone(),
|
|
cfg.common.tracing_header_value.clone(),
|
|
);
|
|
opentelemetry_otlp::SpanExporter::builder()
|
|
.with_http()
|
|
.with_http_client(
|
|
reqwest::Client::builder()
|
|
.danger_accept_invalid_certs(true)
|
|
.timeout(Duration::from_secs(10)) // Overall request timeout
|
|
.connect_timeout(Duration::from_secs(5)) // Connection establishment timeout
|
|
.pool_idle_timeout(Duration::from_secs(60)) // How long to keep idle connections
|
|
.pool_max_idle_per_host(10) // How many idle connections to keep per host
|
|
.build()?,
|
|
)
|
|
.with_endpoint(&cfg.common.otel_otlp_url)
|
|
.with_headers(headers)
|
|
.build()?
|
|
},
|
|
opentelemetry_sdk::runtime::Tokio,
|
|
)
|
|
.build(),
|
|
)
|
|
} else {
|
|
tracer_builder.with_span_processor(
|
|
opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor::builder(
|
|
{
|
|
let mut metadata = MetadataMap::new();
|
|
metadata.insert(
|
|
MetadataKey::from_str(&cfg.common.tracing_header_key).unwrap(),
|
|
MetadataValue::from_str(&cfg.common.tracing_header_value).unwrap(),
|
|
);
|
|
metadata.insert(
|
|
MetadataKey::from_str(&cfg.grpc.org_header_key).unwrap(),
|
|
MetadataValue::from_str(&cfg.common.tracing_grpc_header_org).unwrap(),
|
|
);
|
|
metadata.insert(
|
|
MetadataKey::from_str(&cfg.grpc.stream_header_key).unwrap(),
|
|
MetadataValue::from_str(&cfg.common.tracing_grpc_header_stream_name)
|
|
.unwrap(),
|
|
);
|
|
opentelemetry_otlp::SpanExporter::builder()
|
|
.with_tonic()
|
|
.with_endpoint(&cfg.common.otel_otlp_grpc_url)
|
|
.with_metadata(metadata)
|
|
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
|
|
.build()?
|
|
},
|
|
opentelemetry_sdk::runtime::Tokio,
|
|
)
|
|
.build(),
|
|
)
|
|
};
|
|
log::info!("Main OpenObserve OTLP exporter configured");
|
|
}
|
|
|
|
// Handle AI tracing (enterprise feature)
|
|
#[cfg(feature = "enterprise")]
|
|
{
|
|
use o2_enterprise::enterprise::common::config::get_config as get_o2_config;
|
|
let o2_cfg = get_o2_config();
|
|
|
|
// If AI tracing is enabled but general tracing is NOT enabled,
|
|
// we need to add OpenObserve OTLP exporter for AI traces
|
|
if o2_cfg.ai.tracing_enabled
|
|
&& !cfg.common.tracing_enabled
|
|
&& !cfg.common.tracing_search_enabled
|
|
{
|
|
log::info!("AI tracing enabled independently - configuring OpenObserve OTLP exporter");
|
|
|
|
// Add OpenObserve exporter for AI traces
|
|
if !cfg.common.otel_otlp_url.is_empty() {
|
|
let mut headers = HashMap::new();
|
|
headers.insert(
|
|
cfg.common.tracing_header_key.clone(),
|
|
cfg.common.tracing_header_value.clone(),
|
|
);
|
|
|
|
let oo_exporter = opentelemetry_otlp::SpanExporter::builder()
|
|
.with_http()
|
|
.with_http_client(
|
|
reqwest::Client::builder()
|
|
.danger_accept_invalid_certs(true)
|
|
.timeout(Duration::from_secs(10))
|
|
.connect_timeout(Duration::from_secs(5))
|
|
.pool_idle_timeout(Duration::from_secs(60))
|
|
.pool_max_idle_per_host(10)
|
|
.build()?,
|
|
)
|
|
.with_endpoint(&cfg.common.otel_otlp_url)
|
|
.with_headers(headers)
|
|
.build()?;
|
|
|
|
let oo_processor = opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor::builder(
|
|
oo_exporter,
|
|
opentelemetry_sdk::runtime::Tokio,
|
|
)
|
|
.build();
|
|
|
|
// Wrap with filtering processor to only send AI traces
|
|
let filtered_processor = FilteringSpanProcessor::new(
|
|
oo_processor,
|
|
Some("ai.".to_string()), // Only send spans starting with "ai."
|
|
);
|
|
|
|
tracer_builder = tracer_builder.with_span_processor(filtered_processor);
|
|
log::info!(
|
|
"AI traces (ai.* spans only) will be sent to OpenObserve: {}",
|
|
cfg.common.otel_otlp_url
|
|
);
|
|
} else if !cfg.common.otel_otlp_grpc_url.is_empty() {
|
|
let mut metadata = MetadataMap::new();
|
|
metadata.insert(
|
|
MetadataKey::from_str(&cfg.common.tracing_header_key).unwrap(),
|
|
MetadataValue::from_str(&cfg.common.tracing_header_value).unwrap(),
|
|
);
|
|
|
|
let oo_exporter = opentelemetry_otlp::SpanExporter::builder()
|
|
.with_tonic()
|
|
.with_endpoint(&cfg.common.otel_otlp_grpc_url)
|
|
.with_metadata(metadata)
|
|
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
|
|
.build()?;
|
|
|
|
let oo_processor = opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor::builder(
|
|
oo_exporter,
|
|
opentelemetry_sdk::runtime::Tokio,
|
|
)
|
|
.build();
|
|
|
|
// Wrap with filtering processor to only send AI traces
|
|
let filtered_processor = FilteringSpanProcessor::new(
|
|
oo_processor,
|
|
Some("ai.".to_string()), // Only send spans starting with "ai."
|
|
);
|
|
|
|
tracer_builder = tracer_builder.with_span_processor(filtered_processor);
|
|
log::info!(
|
|
"AI traces (ai.* spans only) will be sent to OpenObserve (gRPC): {}",
|
|
cfg.common.otel_otlp_grpc_url
|
|
);
|
|
} else {
|
|
log::warn!(
|
|
"AI tracing enabled but ZO_OTEL_OTLP_URL not configured - AI traces will not be exported"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Additionally, if O2_AI_EVAL_OTLP_ENDPOINT is set, send AI traces to evaluation platform
|
|
// too
|
|
let eval_endpoint = std::env::var("O2_AI_EVAL_OTLP_ENDPOINT")
|
|
.ok()
|
|
.or_else(|| o2_cfg.ai.eval_otlp_endpoint.clone())
|
|
.filter(|s| !s.is_empty());
|
|
|
|
if let Some(endpoint) = eval_endpoint {
|
|
log::info!(
|
|
"Configuring additional AI evaluation OTLP exporter to: {}",
|
|
endpoint
|
|
);
|
|
|
|
let eval_exporter = opentelemetry_otlp::SpanExporter::builder()
|
|
.with_tonic()
|
|
.with_endpoint(endpoint)
|
|
.with_timeout(std::time::Duration::from_secs(10))
|
|
.build()?;
|
|
|
|
let eval_processor = opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor::builder(
|
|
eval_exporter,
|
|
opentelemetry_sdk::runtime::Tokio,
|
|
)
|
|
.build();
|
|
|
|
// Wrap with filtering processor to only send AI traces
|
|
let filtered_eval_processor =
|
|
FilteringSpanProcessor::new(eval_processor, Some("ai.".to_string()));
|
|
|
|
tracer_builder = tracer_builder.with_span_processor(filtered_eval_processor);
|
|
log::info!(
|
|
"AI evaluation OTLP exporter configured - AI traces (ai.* spans only) will be sent to evaluation platform"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add UUID v7 ID generator and resource attributes
|
|
tracer_builder = tracer_builder.with_id_generator({
|
|
#[cfg(feature = "enterprise")]
|
|
{
|
|
ai::agent::tracing::UuidV7IdGenerator
|
|
}
|
|
#[cfg(not(feature = "enterprise"))]
|
|
{
|
|
opentelemetry_sdk::trace::RandomIdGenerator::default()
|
|
}
|
|
});
|
|
|
|
// Store the tracer provider before installing batch processor
|
|
let tracer = tracer_builder.with_resource(
|
|
Resource::builder()
|
|
.with_attributes(vec![
|
|
KeyValue::new("service.name", cfg.common.node_role.to_string()),
|
|
KeyValue::new("service.instance", cfg.common.instance_name.to_string()),
|
|
KeyValue::new("service.version", config::VERSION),
|
|
])
|
|
.build(),
|
|
);
|
|
|
|
// build
|
|
let tracer = tracer.build();
|
|
|
|
let layer = if cfg.log.json_format {
|
|
tracing_subscriber::fmt::layer()
|
|
.with_ansi(false)
|
|
.json()
|
|
.boxed()
|
|
} else {
|
|
tracing_subscriber::fmt::layer().with_ansi(false).boxed()
|
|
};
|
|
|
|
global::set_tracer_provider(tracer.clone());
|
|
Registry::default()
|
|
.with(tracing_subscriber::EnvFilter::new(&cfg.log.level))
|
|
.with(layer)
|
|
.with(OpenTelemetryLayer::new(
|
|
tracer.tracer("tracing-otel-subscriber"),
|
|
))
|
|
.init();
|
|
|
|
// Return the tracer provider
|
|
Ok(tracer)
|
|
}
|
|
|
|
#[cfg(feature = "enterprise")]
|
|
async fn init_action_server() -> Result<(), anyhow::Error> {
|
|
let cfg = get_config();
|
|
|
|
let haddr: SocketAddr = if cfg.http.ipv6_enabled {
|
|
format!("[::]:{}", cfg.http.port).parse()?
|
|
} else {
|
|
let ip = if !cfg.http.addr.is_empty() {
|
|
cfg.http.addr.clone()
|
|
} else {
|
|
"0.0.0.0".to_string()
|
|
};
|
|
format!("{}:{}", ip, cfg.http.port).parse()?
|
|
};
|
|
|
|
// Setup the namespace
|
|
o2_enterprise::enterprise::actions::action_deployer::init().await?;
|
|
|
|
let scheme = if cfg.http.tls_enabled {
|
|
"HTTPS"
|
|
} else {
|
|
"HTTP"
|
|
};
|
|
log::info!("Starting Action Server {scheme} server at: {haddr}");
|
|
|
|
// Build the router for action server
|
|
let app = create_action_server_router()
|
|
.layer(config::axum::middlewares::AccessLogLayer::new(
|
|
config::axum::middlewares::get_http_access_log_format(),
|
|
))
|
|
.layer(config::axum::middlewares::SlowLogLayer::new(
|
|
cfg.limit.http_slow_log_threshold,
|
|
))
|
|
.layer(TraceLayer::new_for_http());
|
|
|
|
if cfg.http.tls_enabled {
|
|
// TLS server using axum-server
|
|
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(
|
|
&cfg.http.tls_cert_path,
|
|
&cfg.http.tls_key_path,
|
|
)
|
|
.await?;
|
|
|
|
let handle = axum_server::Handle::new();
|
|
let shutdown_timeout = cfg.limit.http_shutdown_timeout;
|
|
|
|
// Spawn task to handle shutdown signal
|
|
tokio::spawn({
|
|
let handle = handle.clone();
|
|
async move {
|
|
shutdown_signal().await;
|
|
handle
|
|
.graceful_shutdown(Some(Duration::from_secs(max(1, shutdown_timeout as u64))));
|
|
}
|
|
});
|
|
|
|
axum_server::bind_rustls(haddr, tls_config)
|
|
.handle(handle)
|
|
.serve(app.into_make_service())
|
|
.await?;
|
|
} else {
|
|
// Non-TLS server
|
|
let listener = TcpListener::bind(haddr).await?;
|
|
axum::serve(listener, app.into_make_service())
|
|
.with_graceful_shutdown(shutdown_signal())
|
|
.await?;
|
|
}
|
|
|
|
log::info!("HTTP server stopped");
|
|
|
|
// flush usage report
|
|
self_reporting::flush().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::handler::http::{request::action_server, router::cors_layer};
|
|
|
|
let cfg = get_config();
|
|
let base_uri = &cfg.common.base_uri;
|
|
|
|
// 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::handler::http::auth::action_server::auth_middleware,
|
|
))
|
|
.layer(cors_layer());
|
|
|
|
// Nest under base URI and set request body size limit
|
|
let router = if base_uri.is_empty() || base_uri == "/" {
|
|
Router::new().nest("/api", api_routes)
|
|
} else {
|
|
Router::new().nest(&format!("{}/api", 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?;
|
|
openobserve::super_cluster_queue::init().await?;
|
|
}
|
|
|
|
// Initialize OpenAPI spec for AI and MCP modules (includes agent client)
|
|
let api = openapi::ApiDoc::openapi();
|
|
if let Err(e) = o2_enterprise::enterprise::ai::init_ai_components(api) {
|
|
log::error!("Failed to init AI/MCP/Agent: {e}");
|
|
} else {
|
|
log::info!("Initialized AI, MCP, and Agent 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}");
|
|
}
|
|
|
|
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::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 meta_store: config::meta::meta_store::MetaStore =
|
|
cfg.common.queue_store.as_str().into();
|
|
if meta_store != config::meta::meta_store::MetaStore::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 tokio::runtime::Runtime;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_setup_logs() {
|
|
let _guard = setup_logs();
|
|
|
|
// Just verify that the guard is valid and the logs setup doesn't panic
|
|
}
|
|
|
|
#[test]
|
|
fn test_enable_tracing_error_handling() {
|
|
// Test that enable_tracing handles configuration errors gracefully
|
|
// This test verifies the function exists and can be called
|
|
// In a real environment, tracing setup might fail due to network issues
|
|
|
|
// We can't easily test the actual tracing setup without mocking external services
|
|
// But we can ensure the function signature and basic error handling work
|
|
let result = std::panic::catch_unwind(|| {
|
|
// Just verify the function can be called
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
// This might fail in test environment due to missing config
|
|
// but that's expected and we're testing error handling
|
|
let _ = enable_tracing();
|
|
});
|
|
});
|
|
|
|
// The test should pass regardless of whether enable_tracing() succeeds or fails
|
|
// In test environments, it may fail due to:
|
|
// 1. Global subscriber already set by another test (when running in parallel)
|
|
// 2. Missing configuration
|
|
// 3. Network issues
|
|
// We're testing that it doesn't panic unexpectedly beyond expected tracing setup issues
|
|
// Don't assert result.is_ok() because parallel tests will fail due to global subscriber
|
|
// conflicts (expected in test environment when tests run in parallel)
|
|
// The important thing is that we can call the function without unexpected panics
|
|
let _ = result;
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resource_key_value_creation() {
|
|
use opentelemetry::KeyValue;
|
|
|
|
// Test KeyValue creation patterns used in tracing setup
|
|
let service_name = KeyValue::new("service.name", "test-service");
|
|
let service_instance = KeyValue::new("service.instance", "test-instance");
|
|
let service_version = KeyValue::new("service.version", "1.0.0");
|
|
|
|
assert_eq!(service_name.key.as_str(), "service.name");
|
|
assert_eq!(service_instance.key.as_str(), "service.instance");
|
|
assert_eq!(service_version.key.as_str(), "service.version");
|
|
}
|
|
|
|
#[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"));
|
|
}
|
|
}
|