diff --git a/Cargo.lock b/Cargo.lock
index fde1bfb27f..5e87ae51ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6770,6 +6770,7 @@ dependencies = [
"o2_enterprise",
"o2_openfga",
"object_store",
+ "openobserve-vrl",
"opentelemetry",
"opentelemetry-proto",
"parking_lot 0.12.5",
@@ -6842,6 +6843,7 @@ dependencies = [
"o2_enterprise",
"o2_openfga",
"openobserve-core",
+ "openobserve-vrl",
"prometheus",
"proto",
"regex",
@@ -6853,6 +6855,18 @@ dependencies = [
"wal",
]
+[[package]]
+name = "openobserve-vrl"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "config",
+ "enrichment",
+ "serde_json",
+ "tokio",
+ "vrl",
+]
+
[[package]]
name = "openssl-probe"
version = "0.2.1"
@@ -9211,14 +9225,20 @@ version = "0.1.0"
dependencies = [
"arrow",
"arrow-schema",
- "async-trait",
+ "chrono",
"config",
- "enrichment",
+ "datafusion",
+ "hashbrown 0.16.1",
+ "infra",
+ "log",
"o2_enterprise",
+ "openobserve-vrl",
+ "parking_lot 0.12.5",
"serde",
"serde_json",
+ "tokio",
+ "tracing",
"utoipa",
- "vrl",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index e0154e68cf..19cc4242e2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -239,6 +239,7 @@ members = [
"src/api_query",
"src/api_common",
"src/core",
+ "src/vrl",
"src/search",
"src/jobs",
"src/config",
@@ -269,6 +270,7 @@ flight = { path = "src/flight" }
tantivy_utils = { path = "src/tantivy_utils" }
web = { path = "src/web" }
openobserve-core = { path = "src/core" }
+openobserve-vrl = { path = "src/vrl" }
search = { path = "src/search" }
openobserve-api = { path = "src/api" }
openobserve-api-management = { path = "src/api_management" }
diff --git a/deny.toml b/deny.toml
index d91a703db8..64502b56e4 100644
--- a/deny.toml
+++ b/deny.toml
@@ -28,6 +28,7 @@ exceptions = [
{ name = "openobserve-api-query", allow = ["AGPL-3.0"] },
{ name = "openobserve-core", allow = ["AGPL-3.0"] },
{ name = "openobserve-jobs", allow = ["AGPL-3.0"] },
+ { name = "openobserve-vrl", allow = ["AGPL-3.0"] },
{ name = "search", allow = ["AGPL-3.0"] },
# Empty dependency-resolution stubs for the private enterprise crates.
{ name = "o2_dex", allow = ["AGPL-3.0"] },
diff --git a/src/config/src/config.rs b/src/config/src/config.rs
index abf1267583..57668ca9da 100644
--- a/src/config/src/config.rs
+++ b/src/config/src/config.rs
@@ -61,6 +61,7 @@ pub static COMMIT_HASH: &str = env!("GIT_COMMIT_HASH");
pub static BUILD_DATE: &str = env!("GIT_BUILD_DATE");
pub const META_ORG_ID: &str = "_meta";
+pub const DEFAULT_ORG: &str = "default";
pub const MMDB_CITY_FILE_NAME: &str = "GeoLite2-City.mmdb";
pub const MMDB_ASN_FILE_NAME: &str = "GeoLite2-ASN.mmdb";
diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml
index 0c85416348..9bbeac11d0 100644
--- a/src/core/Cargo.toml
+++ b/src/core/Cargo.toml
@@ -20,15 +20,17 @@ enterprise = [
"search/enterprise",
]
cloud = ["infra/cloud", "enterprise", "o2_enterprise/cloud"]
-vectorscan = ["o2_enterprise/vectorscan"]
+vectorscan = ["o2_enterprise/vectorscan", "search/vectorscan"]
vortex = [
"dep:vortex",
"dep:vortex-datafusion",
"config/vortex",
"o2_enterprise?/vortex",
+ "search/vortex",
]
[dependencies]
+openobserve-vrl.workspace = true
search.workspace = true
axum.workspace = true
http.workspace = true
diff --git a/src/core/src/cipher/mod.rs b/src/core/src/cipher/mod.rs
index 1db2d28bcb..c3192865c3 100644
--- a/src/core/src/cipher/mod.rs
+++ b/src/core/src/cipher/mod.rs
@@ -13,12 +13,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+pub use ::search::cipher::registry;
use o2_enterprise::enterprise::cipher::http_repr::HttpKey;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
-pub mod registry;
-
#[derive(Clone, Debug, Deserialize, ToSchema)]
pub struct KeyAddRequest {
pub name: String,
diff --git a/src/core/src/common/infra/config.rs b/src/core/src/common/infra/config.rs
index be7563ad74..edd9b1aa5e 100644
--- a/src/core/src/common/infra/config.rs
+++ b/src/core/src/common/infra/config.rs
@@ -21,7 +21,6 @@ use config::{
alerts::alert::Alert,
destinations::{Destination, Template},
folder::Folder,
- function::Transform,
pipeline::Pipeline,
promql::ClusterLeader,
ratelimit::CachedUserRoles,
@@ -33,24 +32,18 @@ use config::{
use dashmap::DashMap;
use hashbrown::HashMap;
use infra::table::short_urls::ShortUrlRecord;
-use parking_lot::RwLock;
-pub use search::enrichment::ENRICHMENT_TABLES;
-use vector_enrichment::TableRegistry;
+pub use openobserve_vrl::{QUERY_FUNCTIONS, enrichment::ENRICHMENT_TABLES};
use crate::{
common::meta::{
maxmind::MaxmindClient,
organization::{OrgStatus, Organization, OrganizationSetting},
},
- service::{
- db::scheduler as db_scheduler, enrichment_table::geoip::Geoip,
- pipeline::batch_execution::ExecutablePipeline,
- },
+ service::{db::scheduler as db_scheduler, pipeline::batch_execution::ExecutablePipeline},
};
// global cache variables
pub static KVS: Lazy> = Lazy::new(Default::default);
-pub static QUERY_FUNCTIONS: Lazy> = Lazy::new(DashMap::default);
pub static USERS: Lazy> =
Lazy::new(DashMap::default);
pub static ORG_USERS: Lazy> =
@@ -74,22 +67,9 @@ pub static REALTIME_ALERT_TRIGGERS: Lazy> = Lazy::new(Default::default);
pub static DESTINATIONS: Lazy> = Lazy::new(Default::default);
-pub static ENRICHMENT_REGISTRY: Lazy> =
- Lazy::new(|| Arc::new(TableRegistry::default()));
-
pub static MAXMIND_DB_CLIENT: Lazy>>> =
Lazy::new(|| Arc::new(tokio::sync::RwLock::new(None)));
-pub static GEOIP_CITY_TABLE: Lazy>>> =
- Lazy::new(|| Arc::new(RwLock::new(None)));
-
-pub static GEOIP_ASN_TABLE: Lazy>>> =
- Lazy::new(|| Arc::new(RwLock::new(None)));
-
-#[cfg(feature = "enterprise")]
-pub static GEOIP_ENT_TABLE: Lazy>>> =
- Lazy::new(|| Arc::new(RwLock::new(None)));
-
pub static STREAM_EXECUTABLE_PIPELINES: Lazy>> =
Lazy::new(Default::default);
pub static PIPELINE_STREAM_MAPPING: Lazy> =
@@ -139,10 +119,6 @@ mod tests {
// Test Arc variables
let users_rum_token = USERS_RUM_TOKEN.clone();
assert_eq!(users_rum_token.len(), 0);
-
- // Test enrichment registry
- let registry = ENRICHMENT_REGISTRY.clone();
- assert!(!std::ptr::eq(registry.as_ref(), std::ptr::null()));
}
#[tokio::test]
@@ -258,29 +234,6 @@ mod tests {
assert!(read_guard.is_none());
}
- #[test]
- fn test_geoip_tables() {
- // Test GEOIP_CITY_TABLE
- let city_table = GEOIP_CITY_TABLE.clone();
- let read_guard = city_table.read();
- assert!(read_guard.is_none());
- drop(read_guard);
-
- // Test GEOIP_ASN_TABLE
- let asn_table = GEOIP_ASN_TABLE.clone();
- let read_guard = asn_table.read();
- assert!(read_guard.is_none());
- drop(read_guard);
-
- #[cfg(feature = "enterprise")]
- {
- // Test GEOIP_ENT_TABLE (only available with enterprise feature)
- let ent_table = GEOIP_ENT_TABLE.clone();
- let read_guard = ent_table.read();
- assert!(read_guard.is_none());
- }
- }
-
#[test]
fn test_dashmap_operations() {
// Test basic DashMap operations on KVS
@@ -303,6 +256,8 @@ mod tests {
#[test]
fn test_query_functions_cache() {
+ use config::meta::function::Transform;
+
let test_key = "test_function".to_string();
let test_transform = Transform {
function: "test_function".to_string(),
diff --git a/src/core/src/common/meta/organization.rs b/src/core/src/common/meta/organization.rs
index 8dc431128a..7de9bc19c4 100644
--- a/src/core/src/common/meta/organization.rs
+++ b/src/core/src/common/meta/organization.rs
@@ -13,14 +13,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+pub use config::DEFAULT_ORG;
use config::{meta::user::UserRole, stats::MemorySize};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[cfg(feature = "cloud")]
use super::user::InviteStatus;
-
-pub const DEFAULT_ORG: &str = "default";
pub const CUSTOM: &str = "custom";
pub const USER_DEFAULT: &str = "user_default";
pub const THRESHOLD: i64 = 9383939382;
diff --git a/src/core/src/common/utils/functions.rs b/src/core/src/common/utils/functions.rs
index 41e79bc064..ce436476b2 100644
--- a/src/core/src/common/utils/functions.rs
+++ b/src/core/src/common/utils/functions.rs
@@ -5,7 +5,7 @@
// 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
+// 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.
@@ -13,160 +13,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::collections::HashMap;
-
-use config::{
- GEO_IP_ASN_ENRICHMENT_TABLE, GEO_IP_CITY_ENRICHMENT_TABLE, meta::function::VRLCompilerConfig,
+pub use openobserve_vrl::{
+ get_all_transform_keys, get_enrichment_tables, get_vrl_compiler_config, init_vrl_runtime,
};
-use vector_enrichment::{Table, TableRegistry};
-
-use crate::common::{
- infra::config::{ENRICHMENT_TABLES, GEOIP_ASN_TABLE, GEOIP_CITY_TABLE},
- meta::organization::DEFAULT_ORG,
-};
-
-pub async fn get_all_transform_keys(org_id: &str) -> Vec {
- let org_key = &format!("{org_id}/");
-
- crate::common::infra::config::QUERY_FUNCTIONS
- .clone()
- .iter()
- .filter_map(|transform| {
- let key = transform.key();
- key.strip_prefix(org_key).map(|x| x.to_string())
- })
- .collect()
-}
-
-pub fn init_vrl_runtime() -> vrl::compiler::runtime::Runtime {
- vrl::compiler::runtime::Runtime::new(vrl::prelude::state::RuntimeState::default())
-}
-
-pub fn get_vrl_compiler_config(org_id: &str) -> VRLCompilerConfig {
- let en_tables = ENRICHMENT_TABLES.clone();
- let mut functions = vrl::stdlib::all();
- functions.append(&mut vector_enrichment::vrl_functions());
- let registry = TableRegistry::default();
- let mut tables: HashMap> = HashMap::new();
-
- for table in en_tables.iter() {
- if table.org_id == org_id || table.org_id == DEFAULT_ORG {
- tables.insert(
- table.stream_name.to_owned(),
- Box::new(table.value().clone()),
- );
- }
- }
- drop(en_tables);
-
- if let Some(v) = GEOIP_CITY_TABLE.read().as_ref() {
- tables.insert(GEO_IP_CITY_ENRICHMENT_TABLE.to_owned(), Box::new(v.clone()));
- }
-
- if let Some(v) = GEOIP_ASN_TABLE.read().as_ref() {
- tables.insert(GEO_IP_ASN_ENRICHMENT_TABLE.to_owned(), Box::new(v.clone()));
- }
-
- #[cfg(feature = "enterprise")]
- if o2_enterprise::enterprise::common::config::get_config()
- .common
- .enable_enterprise_mmdb
- {
- let geoip_ent = crate::common::infra::config::GEOIP_ENT_TABLE.read();
- if let Some(table) = geoip_ent.as_ref() {
- tables.insert(
- o2_enterprise::enterprise::common::config::GEO_IP_ENTERPRISE_ENRICHMENT_TABLE
- .to_owned(),
- Box::new(table.clone()),
- );
- }
- };
-
- registry.load(tables);
- let mut config = vrl::compiler::CompileConfig::default();
- config.set_custom(registry);
- VRLCompilerConfig { config, functions }
-}
-
-#[cfg(test)]
-mod tests {
-
- use std::sync::Arc;
-
- use super::*;
-
- #[test]
- fn test_init_vrl_runtime() {
- let runtime = init_vrl_runtime();
- assert!(runtime.is_empty());
- }
-
- #[test]
- fn test_get_vrl_compiler_config() {
- let config = get_vrl_compiler_config("default");
- assert!(!config.functions.is_empty());
- assert!(config.config.get_custom::().is_some());
- }
-
- #[tokio::test]
- async fn test_get_all_transform_keys() {
- // Setup test data
- let test_org = "test_org";
- let test_key = format!("{test_org}/test_transform");
- crate::common::infra::config::QUERY_FUNCTIONS.insert(
- test_key.clone(),
- config::meta::function::Transform {
- name: test_key.clone(),
- function: ".".to_string(),
- params: "row".to_string(),
- num_args: 1,
- trans_type: Some(0),
- streams: Some(vec![config::meta::function::StreamOrder {
- stream: "test".to_string(),
- order: 1,
- stream_type: config::meta::stream::StreamType::Logs,
- is_removed: false,
- apply_before_flattening: false,
- }]),
- },
- );
-
- // Test the function
- let keys = get_all_transform_keys(test_org).await;
- assert!(keys.contains(&"test_transform".to_string()));
- }
-
- #[test]
- fn test_get_vrl_compiler_config_with_enrichment_tables() {
- // Setup test data
- let test_org = "test_org";
- let test_stream = "test_stream";
- let table_name = "test_table";
- let en_table = crate::service::enrichment::StreamTable {
- org_id: test_org.to_string(),
- stream_name: test_stream.to_string(),
- data: Arc::new(vec![]),
- };
- ENRICHMENT_TABLES.insert(table_name.to_string(), en_table);
-
- // Test the function
- let config = get_vrl_compiler_config(test_org);
- let registry = config.config.get_custom::().unwrap();
-
- // Verify enrichment table is loaded
- assert!(registry.table_ids().contains(&test_stream.to_string()));
- }
-
- #[tokio::test]
- async fn test_get_all_transform_keys_empty() {
- let keys = get_all_transform_keys("nonexistent_org").await;
- assert!(keys.is_empty());
- }
-
- #[test]
- fn test_get_vrl_compiler_config_empty() {
- let config = get_vrl_compiler_config("nonexistent_org");
- let registry = config.config.get_custom::().unwrap();
- assert!(registry.table_ids().is_empty());
- }
-}
diff --git a/src/core/src/service/file_downloader.rs b/src/core/src/service/file_downloader.rs
index 0a7fad02d2..ce055621b7 100644
--- a/src/core/src/service/file_downloader.rs
+++ b/src/core/src/service/file_downloader.rs
@@ -13,720 +13,4 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::{
- collections::VecDeque,
- sync::{Arc, LazyLock as Lazy},
-};
-
-use config::{
- cluster::LOCAL_NODE,
- get_config,
- meta::cluster::{Role, RoleGroup, get_internal_grpc_token},
- metrics,
- utils::time::{day_micros, now_micros},
-};
-use futures_util::StreamExt;
-use hashbrown::{HashMap, HashSet};
-use infra::{cache::file_data, cluster};
-use proto::cluster_rpc::{SimpleFileList, event_client::EventClient};
-use tokio::sync::{
- Mutex,
- mpsc::{Receiver, Sender},
-};
-use tonic::{codec::CompressionEncoding, metadata::MetadataValue};
-
-/// (trace_id, file_id, account, file, size, cache_type)
-type FileInfo = (String, i64, String, String, usize, file_data::CacheType);
-
-mod processing_files {
- use hashbrown::HashSet;
- use parking_lot::RwLock;
-
- use super::*;
-
- static PROCESSING_FILES: Lazy>> =
- Lazy::new(|| RwLock::new(HashSet::new()));
-
- pub fn is_processing(file_name: &str) -> bool {
- PROCESSING_FILES.read().contains(file_name)
- }
-
- pub fn add(file_name: &str) {
- PROCESSING_FILES.write().insert(file_name.to_string());
- }
-
- pub fn remove(file_name: &str) {
- PROCESSING_FILES.write().remove(file_name);
- }
-}
-
-mod queued_files {
- use hashbrown::HashSet;
- use parking_lot::RwLock;
-
- use super::*;
-
- static QUEUED_FILES: Lazy>> = Lazy::new(|| RwLock::new(HashSet::new()));
-
- /// Returns true if newly inserted (caller should enqueue), false if already present (skip).
- pub fn try_add(file_name: &str) -> bool {
- QUEUED_FILES.write().insert(file_name.to_string())
- }
-
- pub fn remove(file_name: &str) {
- QUEUED_FILES.write().remove(file_name);
- }
-}
-
-struct DownloadQueue {
- sender: Sender,
- receiver: Arc>>,
-}
-
-impl DownloadQueue {
- fn new(sender: Sender, receiver: Arc>>) -> Self {
- Self { sender, receiver }
- }
-}
-
-struct PriorityDownloadQueue {
- stack: Mutex>,
- notify: tokio::sync::Notify,
- max_size: usize,
-}
-
-impl PriorityDownloadQueue {
- fn new(max_size: usize) -> Self {
- Self {
- stack: Mutex::new(VecDeque::with_capacity(max_size)),
- notify: tokio::sync::Notify::new(),
- max_size,
- }
- }
-
- // Returns false if at cap — caller must remove from queued_files and skip metric inc.
- async fn push(&self, file_info: FileInfo) -> bool {
- let mut stack = self.stack.lock().await;
- if stack.len() >= self.max_size {
- return false;
- }
- stack.push_back(file_info);
- drop(stack);
- self.notify.notify_one();
- true
- }
-
- // Blocks until an item is available. LIFO via pop_back.
- // notified() is created before the lock check to avoid missed-wakeup race.
- // Chains notify_one() so all workers drain the queue under burst load.
- async fn pop(&self) -> FileInfo {
- loop {
- let notified = self.notify.notified();
- {
- let mut stack = self.stack.lock().await;
- if let Some(item) = stack.pop_back() {
- if !stack.is_empty() {
- self.notify.notify_one();
- }
- return item;
- }
- }
- notified.await;
- }
- }
-}
-
-const FILE_DOWNLOAD_QUEUE_SIZE: usize = 10000;
-static FILE_DOWNLOAD_CHANNEL: Lazy = Lazy::new(|| {
- let (tx, rx) = tokio::sync::mpsc::channel::(FILE_DOWNLOAD_QUEUE_SIZE);
- DownloadQueue::new(tx, Arc::new(Mutex::new(rx)))
-});
-
-static PRIORITY_FILE_DOWNLOAD_CHANNEL: Lazy =
- Lazy::new(|| PriorityDownloadQueue::new(FILE_DOWNLOAD_QUEUE_SIZE));
-
-pub async fn run() -> Result<(), anyhow::Error> {
- let cfg = get_config();
- // worker tasks: handle normal queue download (FIFO via mpsc channel)
- for thread in 0..cfg.limit.file_download_thread_num {
- let rx = FILE_DOWNLOAD_CHANNEL.receiver.clone();
- tokio::spawn(async move {
- loop {
- let ret = rx.lock().await.recv().await;
- match ret {
- None => {
- log::debug!("[FILE_CACHE_DOWNLOAD:JOB:NORMAL] Receiving channel is closed");
- break;
- }
- Some((trace_id, id, account, file, file_size, cache)) => {
- // transition: queued → not-queued (must run before any skip path)
- queued_files::remove(&file);
-
- // check if the file is already being downloaded
- if processing_files::is_processing(&file) {
- log::warn!(
- "[trace_id {trace_id}] [thread {thread}] search->storage: file {file} is already being downloaded, will skip it"
- );
- // update metrics
- metrics::FILE_DOWNLOADER_NORMAL_QUEUE_SIZE
- .with_label_values::<&str>(&[])
- .dec();
- continue;
- }
-
- // add the file to processing set
- processing_files::add(&file);
-
- // download the file
- match download_file(
- thread, &trace_id, id, &account, &file, file_size, cache,
- )
- .await
- {
- Ok(data_len) => {
- if data_len > 0 && data_len != file_size {
- log::warn!(
- "[FILE_CACHE_DOWNLOAD:JOB:NORMAL] download file {file} found size mismatch, expected: {file_size}, actual: {data_len}, will skip it",
- );
- }
- }
- Err(e) => {
- log::error!(
- "[FILE_CACHE_DOWNLOAD:JOB:NORMAL] download file {file} to cache {cache:?} err: {e}",
- );
- }
- }
-
- // remove the file from processing set
- processing_files::remove(&file);
-
- // update metrics
- metrics::FILE_DOWNLOADER_NORMAL_QUEUE_SIZE
- .with_label_values::<&str>(&[])
- .dec();
- }
- }
- }
- });
- }
-
- // worker tasks: handle priority queue download (LIFO via VecDeque::pop_back)
- for thread in 0..cfg.limit.file_download_priority_queue_thread_num {
- tokio::spawn(async move {
- loop {
- let (trace_id, id, account, file, file_size, cache) =
- PRIORITY_FILE_DOWNLOAD_CHANNEL.pop().await;
-
- // transition: queued → not-queued (must run before any skip path)
- queued_files::remove(&file);
-
- // check if the file is already being downloaded
- if processing_files::is_processing(&file) {
- log::warn!(
- "[trace_id {trace_id}] [thread {thread}] search->storage: file {file} is already being downloaded, will skip it"
- );
- metrics::FILE_DOWNLOADER_PRIORITY_QUEUE_SIZE
- .with_label_values::<&str>(&[])
- .dec();
- continue;
- }
-
- // add the file to processing set
- processing_files::add(&file);
-
- // download the file
- match download_file(thread, &trace_id, id, &account, &file, file_size, cache).await
- {
- Ok(data_len) => {
- if data_len > 0 && data_len != file_size {
- log::warn!(
- "[FILE_CACHE_DOWNLOAD:JOB:PRIORITY] download file {file} found size mismatch, expected: {file_size}, actual: {data_len}, will skip it",
- );
- }
- }
- Err(e) => {
- log::error!(
- "[FILE_CACHE_DOWNLOAD:JOB:PRIORITY] download file {file} to cache {cache:?} err: {e}",
- );
- }
- }
-
- // remove the file from processing set
- processing_files::remove(&file);
-
- // update metrics
- metrics::FILE_DOWNLOADER_PRIORITY_QUEUE_SIZE
- .with_label_values::<&str>(&[])
- .dec();
- }
- });
- }
- Ok(())
-}
-
-async fn download_file(
- thread: usize,
- trace_id: &str,
- file_id: i64,
- account: &str,
- file_name: &str,
- file_size: usize,
- cache_type: file_data::CacheType,
-) -> Result {
- let cfg = get_config();
-
- // download file from node
- if cfg.cache_latest_files.download_from_node
- && let Ok(ok) =
- download_file_with_consistent_hash(file_id, account, file_name, file_size).await
- && ok
- {
- return Ok(file_size);
- }
-
- // download from object store
- let size = if file_size > 0 { Some(file_size) } else { None };
- let start = std::time::Instant::now();
- let ret = match cache_type {
- file_data::CacheType::Memory => {
- let mut disk_exists = false;
- let mem_exists = file_data::memory::exist(file_name).await;
- if !mem_exists && !cfg.memory_cache.skip_disk_check {
- disk_exists = file_data::disk::exist(file_name).await;
- }
- if !mem_exists && (cfg.memory_cache.skip_disk_check || !disk_exists) {
- file_data::memory::download(account, file_name, size).await
- } else {
- Ok(0)
- }
- }
- file_data::CacheType::Disk => {
- if !file_data::disk::exist(file_name).await {
- file_data::disk::download(account, file_name, size).await
- } else {
- Ok(0)
- }
- }
- _ => Ok(0),
- };
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:JOB:{thread}] [trace_id {trace_id}] download file: {file_name}, ret: {:?}, took: {} ms",
- ret,
- start.elapsed().as_millis()
- );
- ret
-}
-
-async fn download_file_with_consistent_hash(
- file_id: i64,
- account: &str,
- file_name: &str,
- file_size: usize,
-) -> Result {
- let role_group = if LOCAL_NODE.is_interactive_querier() {
- RoleGroup::Interactive
- } else {
- RoleGroup::Background
- };
- let Some(node_name) = cluster::get_node_from_consistent_hash(
- &file_id.to_string(),
- &Role::Querier,
- Some(role_group),
- )
- .await
- else {
- return Ok(false);
- };
- // get node by file_id
- let Some(node) = cluster::get_cached_node_by_name(&node_name).await else {
- return Ok(false);
- };
- // download file from node
- let Ok(failed) = download_from_node(
- &node.grpc_addr,
- &[(
- file_id,
- account.to_string(),
- file_name.to_string(),
- file_size as i64,
- 0,
- )],
- )
- .await
- else {
- return Ok(false);
- };
- // failed is empty means download success
- Ok(failed.is_empty())
-}
-
-// download files from node and return download failed files
-// file: (account, file, size, ts)
-pub async fn download_from_node(
- addr: &str,
- files: &[(i64, String, String, i64, i64)],
-) -> Result, anyhow::Error> {
- let start = std::time::Instant::now();
- let cfg = get_config();
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:gRPC] Download files from node start, files: {:?}",
- files.len()
- );
-
- let token: MetadataValue<_> = get_internal_grpc_token()
- .parse()
- .map_err(|_| anyhow::anyhow!("Invalid token"))?;
-
- let channel = infra::client::grpc::get_cached_channel(addr).await?;
- let client = EventClient::with_interceptor(channel, move |mut req: tonic::Request<()>| {
- req.metadata_mut().insert("authorization", token.clone());
- Ok(req)
- });
-
- let file_size_map = files
- .iter()
- .filter_map(|(_, _, f, s, _)| {
- if *s > cfg.cache_latest_files.download_node_size * 1024 * 1024 {
- None
- } else {
- Some((f, *s as usize))
- }
- })
- .collect::>();
- if file_size_map.is_empty() {
- return Ok(files.to_vec());
- }
- let request = tonic::Request::new(SimpleFileList {
- files: files.iter().map(|(_, _, f, ..)| f.to_string()).collect(),
- });
-
- let resp = client
- .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)
- .get_files(request)
- .await
- .map_err(|e| anyhow::anyhow!("Failed to get files from {addr}, {e}"))?;
-
- let mut file_contents = HashMap::new();
- let mut downloaded_files = HashSet::new();
- let mut resp_stream = resp.into_inner();
- while let Some(resp) = resp_stream.next().await {
- let resp = match resp {
- Ok(resp) => resp,
- Err(err) => {
- if err.code() == tonic::Code::NotFound {
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:gRPC] Failed to download file {} from {}: file not found",
- err.message(),
- addr
- );
- continue;
- }
- return Err(anyhow::anyhow!(
- "Failed to download file from {addr}, {err}"
- ));
- }
- };
- for content in resp.entries {
- let entry = file_contents
- .entry(content.filename.clone())
- .or_insert(bytes::BytesMut::new());
- entry.extend_from_slice(&content.content);
- downloaded_files.insert(content.filename);
- }
- }
-
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:gRPC] Successfully retrieved {} files from {} in {} ms",
- downloaded_files.len(),
- addr,
- start.elapsed().as_millis()
- );
-
- // Cache the file contents
- for (file, content) in file_contents {
- let data = content.freeze();
- if let Some(size) = file_size_map.get(&file)
- && *size != data.len()
- {
- log::warn!(
- "[FILE_CACHE_DOWNLOAD:gRPC] Failed to download file {} from {}: size mismatch, expected {} but got {}",
- file,
- addr,
- size,
- data.len()
- );
- downloaded_files.remove(&file);
- continue;
- }
- if let Err(e) = infra::cache::file_data::set(&file, data).await {
- log::error!("[FILE_CACHE_DOWNLOAD:gRPC] Failed to cache file {file}: {e}");
- downloaded_files.remove(&file);
- }
- }
-
- // Return list of failed files
- let failed_files: Vec<_> = files
- .iter()
- .filter(|(_, f, ..)| !downloaded_files.contains(f))
- .cloned()
- .collect();
-
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:gRPC] Failed to retrieve {} files from {} in {} ms",
- failed_files.len(),
- addr,
- start.elapsed().as_millis()
- );
-
- Ok(failed_files)
-}
-
-pub async fn queue_download(
- trace_id: String,
- id: i64,
- account: String,
- file: String,
- size: i64,
- ts: i64,
- cache_type: file_data::CacheType,
-) -> Result<(), anyhow::Error> {
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:JOB] [trace_id {trace_id}] enqueue file: {file}, size: {size}, ts: {ts}"
- );
-
- // files with data older than the cache max age are not worth caching: with
- // time_lru they become the oldest bucket and are evicted first, so the
- // download would be wasted work. Skip the queue and let the query read
- // them directly from object storage.
- if exceeds_cache_max_age(ts, cache_type) {
- log::debug!(
- "[FILE_CACHE_DOWNLOAD:JOB] [trace_id {trace_id}] skip file: {file}, data is older than the cache max age"
- );
- return Ok(());
- }
-
- let cfg = get_config();
-
- // skip if already queued or already being downloaded
- if !queued_files::try_add(&file) {
- return Ok(());
- }
- if processing_files::is_processing(&file) {
- queued_files::remove(&file);
- return Ok(());
- }
-
- if cfg.limit.file_download_enable_priority_queue
- && should_prioritize_file(ts, cfg.limit.file_download_priority_queue_window_secs)
- {
- if PRIORITY_FILE_DOWNLOAD_CHANNEL
- .push((
- trace_id,
- id,
- account,
- file.clone(),
- size as usize,
- cache_type,
- ))
- .await
- {
- metrics::FILE_DOWNLOADER_PRIORITY_QUEUE_SIZE
- .with_label_values::<&str>(&[])
- .inc();
- } else {
- queued_files::remove(&file);
- log::warn!(
- "[FILE_CACHE_DOWNLOAD:JOB] priority queue full ({FILE_DOWNLOAD_QUEUE_SIZE}), dropping file: {file}"
- );
- }
- } else {
- if let Err(e) = FILE_DOWNLOAD_CHANNEL
- .sender
- .send((
- trace_id,
- id,
- account,
- file.clone(),
- size as usize,
- cache_type,
- ))
- .await
- {
- queued_files::remove(&file);
- return Err(e.into());
- }
- metrics::FILE_DOWNLOADER_NORMAL_QUEUE_SIZE
- .with_label_values::<&str>(&[])
- .inc();
- }
- Ok(())
-}
-
-// if the file timestamp is in the past window, it should be prioritized
-fn should_prioritize_file(ts: i64, window_secs: i64) -> bool {
- let window_micros = window_secs * 1_000_000;
- let now = now_micros();
- ts > now - window_micros
-}
-
-/// Returns true if the file's data is older than the cache max age and should
-/// not be downloaded into the cache.
-pub fn exceeds_cache_max_age(ts: i64, cache_type: file_data::CacheType) -> bool {
- let cfg = get_config();
- let max_age_days = match cache_type {
- file_data::CacheType::Memory => cfg.memory_cache.max_age_days,
- file_data::CacheType::Disk => cfg.disk_cache.max_age_days,
- file_data::CacheType::None => 0,
- };
- exceeds_max_age(ts, max_age_days)
-}
-
-// if the file data is older than max_age_days, it should not be cached.
-// max_age_days == 0 means no limit, ts <= 0 means the timestamp is unknown.
-fn exceeds_max_age(ts: i64, max_age_days: i64) -> bool {
- if max_age_days <= 0 || ts <= 0 {
- return false;
- }
- ts < now_micros() - day_micros(max_age_days)
-}
-
-#[cfg(test)]
-mod tests {
- use config::utils::time::{day_micros, hour_micros, now_micros};
-
- use super::{
- FileInfo, PriorityDownloadQueue, exceeds_max_age, file_data, processing_files, queued_files,
- };
-
- #[test]
- fn test_exceeds_max_age_disabled() {
- // max_age_days == 0 means no limit, nothing is too old
- let one_year_ago = now_micros() - day_micros(365);
- assert!(!exceeds_max_age(one_year_ago, 0));
- assert!(!exceeds_max_age(one_year_ago, -1));
- }
-
- #[test]
- fn test_exceeds_max_age_unknown_ts() {
- // unknown timestamp should not be skipped
- assert!(!exceeds_max_age(0, 3));
- assert!(!exceeds_max_age(-1, 3));
- }
-
- #[test]
- fn test_exceeds_max_age_recent_file() {
- let one_hour_ago = now_micros() - hour_micros(1);
- assert!(!exceeds_max_age(one_hour_ago, 3));
- }
-
- #[test]
- fn test_exceeds_max_age_old_file() {
- let thirty_days_ago = now_micros() - day_micros(30);
- assert!(exceeds_max_age(thirty_days_ago, 3));
- }
-
- #[test]
- fn test_is_processing_false_initially() {
- assert!(!processing_files::is_processing(
- "unique_not_added_file_abc123.parquet"
- ));
- }
-
- #[test]
- fn test_add_and_is_processing() {
- let name = "test_add_file_xyz999.parquet";
- processing_files::add(name);
- assert!(processing_files::is_processing(name));
- processing_files::remove(name);
- }
-
- #[test]
- fn test_remove_clears_processing() {
- let name = "test_remove_file_qqq888.parquet";
- processing_files::add(name);
- processing_files::remove(name);
- assert!(!processing_files::is_processing(name));
- }
-
- #[test]
- fn test_normal_queue_cap_honored() {
- let cap = 3;
- let (tx, _rx) = tokio::sync::mpsc::channel::(cap);
-
- for i in 0..cap {
- let result = tx.try_send((
- format!("trace-{i}"),
- i as i64,
- "org".into(),
- format!("file-{i}.parquet"),
- 1024,
- file_data::CacheType::Disk,
- ));
- assert!(result.is_ok(), "send {i} should succeed under cap");
- }
-
- let overflow = tx.try_send((
- "trace-overflow".into(),
- 99,
- "org".into(),
- "overflow.parquet".into(),
- 1024,
- file_data::CacheType::Disk,
- ));
- assert!(overflow.is_err(), "send beyond cap must fail");
- }
-
- #[test]
- fn test_queued_files_no_duplicates() {
- let file = "dedup_test_unique_zz1234.parquet";
-
- // first add succeeds
- assert!(queued_files::try_add(file));
- // same file rejected while already queued
- assert!(!queued_files::try_add(file));
-
- // after remove, can be queued again
- queued_files::remove(file);
- assert!(queued_files::try_add(file));
-
- // cleanup
- queued_files::remove(file);
- }
-
- #[tokio::test]
- async fn test_priority_queue_cap_honored() {
- let cap = 3;
- let queue = PriorityDownloadQueue::new(cap);
-
- // fill to cap — all succeed
- for i in 0..cap {
- let ok = queue
- .push((
- format!("trace-{i}"),
- i as i64,
- "org".into(),
- format!("file-{i}.parquet"),
- 1024,
- file_data::CacheType::Disk,
- ))
- .await;
- assert!(ok, "push {i} should succeed under cap");
- }
-
- // one more must be rejected
- let overflow = queue
- .push((
- "trace-overflow".into(),
- 99,
- "org".into(),
- "overflow.parquet".into(),
- 1024,
- file_data::CacheType::Disk,
- ))
- .await;
- assert!(!overflow, "push beyond cap must return false");
- }
-}
+pub use infra::cache::file_downloader::*;
diff --git a/src/core/src/service/ingestion/mod.rs b/src/core/src/service/ingestion/mod.rs
index da1319d611..8c5b8589b3 100644
--- a/src/core/src/service/ingestion/mod.rs
+++ b/src/core/src/service/ingestion/mod.rs
@@ -29,7 +29,7 @@ use config::{
ider::SnowflakeIdGenerator,
meta::{
alerts::alert::Alert,
- function::{VRLResultResolver, VRLRuntimeConfig},
+ function::VRLResultResolver,
self_reporting::usage::{RequestStats, TriggerData, TriggerDataStatus, TriggerDataType},
stream::{PartitionTimeLevel, StreamParams, StreamPartition, StreamType},
},
@@ -41,10 +41,7 @@ use infra::{
schema::STREAM_RECORD_ID_GENERATOR,
};
use proto::cluster_rpc::IngestionType;
-use vrl::{
- compiler::{CompilationResult, TargetValueRef, runtime::Runtime},
- prelude::state,
-};
+use vrl::compiler::{TargetValueRef, runtime::Runtime};
use super::{
db::{alerts::alert, pipeline},
@@ -55,11 +52,8 @@ use crate::{
common::{
infra::config::{REALTIME_ALERT_TRIGGERS, STREAM_ALERTS},
meta::{ingestion::IngestionRequest, stream::SchemaRecords},
- utils::{
- functions::get_vrl_compiler_config,
- js::{
- JSRuntimeConfig, apply_js_fn as apply_js, compile_js_function as compile_js_func,
- },
+ utils::js::{
+ JSRuntimeConfig, apply_js_fn as apply_js, compile_js_function as compile_js_func,
},
},
service::{
@@ -72,6 +66,8 @@ use crate::{
pub mod grpc;
pub mod ingestion_service;
+pub use openobserve_vrl::compile_vrl_function;
+
pub type TriggerAlertData = Vec<(Alert, Vec