diff --git a/Cargo.lock b/Cargo.lock
index d2c45a28fe9..7454707d1c3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9949,6 +9949,7 @@ dependencies = [
"common-catalog",
"common-datasource",
"common-error",
+ "common-event-recorder",
"common-frontend",
"common-function",
"common-grpc",
diff --git a/config/config.md b/config/config.md
index 90c346d09ee..7f9064300a6 100644
--- a/config/config.md
+++ b/config/config.md
@@ -233,7 +233,7 @@
| `tracing.tokio_console_addr` | String | Unset | The tokio console address. |
| `event_recorder` | -- | -- | Configuration options for the event recorder. |
| `event_recorder.ttl` | String | `90d` | TTL for the events table that will be used to store the events. Default is `90d`. |
-| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `create_database`,
`alter_database`, `drop_database`, `create_flow`, `drop_flow`,
`create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`,
`drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`,
`create_view`, `drop_view`.
When omitted, all current and future event types are recorded.
Set to an empty array to disable event recording. |
+| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `create_database`,
`alter_database`, `drop_database`, `create_flow`, `drop_flow`,
`create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`,
`drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`,
`create_view`, `drop_view`, `admin_function`.
When omitted, all current and future event types are recorded.
Set to an empty array to disable event recording. |
| `memory` | -- | -- | The memory options. |
| `memory.enable_heap_profiling` | Bool | `true` | Whether to enable heap profiling activation during startup.
When enabled, heap profiling will be activated if the `MALLOC_CONF` environment variable
is set to "prof:true,prof_active:false". The official image adds this env variable.
Default is true. |
@@ -370,6 +370,7 @@
| `memory.enable_heap_profiling` | Bool | `true` | Whether to enable heap profiling activation during startup.
When enabled, heap profiling will be activated if the `MALLOC_CONF` environment variable
is set to "prof:true,prof_active:false". The official image adds this env variable.
Default is true. |
| `event_recorder` | -- | -- | Configuration options for the event recorder. |
| `event_recorder.ttl` | String | `90d` | TTL for the events table that will be used to store the events. Default is `90d`. |
+| `event_recorder.event_types` | Array | -- | Event types to record. Current available event type: `admin_function`.
When omitted, all current and future event types are recorded.
Set to an empty array to disable event recording. |
### Metasrv
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index f495f3b19f1..30e5f1ff392 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -402,3 +402,7 @@ enable_heap_profiling = true
[event_recorder]
## TTL for the events table that will be used to store the events. Default is `90d`.
ttl = "90d"
+## Event types to record. Current available event type: `admin_function`.
+## When omitted, all current and future event types are recorded.
+## Set to an empty array to disable event recording.
+#+ event_types = ["admin_function"]
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index 4abbf815c06..8ced8b17906 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -919,7 +919,7 @@ ttl = "90d"
## `alter_database`, `drop_database`, `create_flow`, `drop_flow`,
## `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`,
## `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`,
-## `create_view`, `drop_view`.
+## `create_view`, `drop_view`, `admin_function`.
## When omitted, all current and future event types are recorded.
## Set to an empty array to disable event recording.
#+ event_types = ["create_database"]
diff --git a/src/cmd/tests/load_config_test.rs b/src/cmd/tests/load_config_test.rs
index bdbaf720b7d..fe06d700117 100644
--- a/src/cmd/tests/load_config_test.rs
+++ b/src/cmd/tests/load_config_test.rs
@@ -445,6 +445,11 @@ fn test_load_event_types_from_env() {
.component
.event_recorder
.event_types,
+ GreptimeOptions::::load_layered_options(None, env_prefix)
+ .unwrap()
+ .component
+ .event_recorder
+ .event_types,
] {
assert!(event_types.allows("region_migration"));
assert!(!event_types.allows("other_event"));
diff --git a/src/common/event-recorder/src/event_table.rs b/src/common/event-recorder/src/event_table.rs
index 0ec38c8eabc..1ea4369a00a 100644
--- a/src/common/event-recorder/src/event_table.rs
+++ b/src/common/event-recorder/src/event_table.rs
@@ -88,6 +88,9 @@ pub const TIMESTAMP_COLUMN: EventTableColumn = EventTableColumn::new(
ColumnDataType::TimestampNanosecond,
SemanticType::Timestamp,
);
+/// The effective database user that initiated an event.
+pub const ACTOR_COLUMN: EventTableColumn =
+ EventTableColumn::new("actor", ColumnDataType::String, SemanticType::Field);
/// The canonical procedure identifier envelope column.
pub const PROCEDURE_ID_COLUMN: EventTableColumn =
EventTableColumn::new("procedure_id", ColumnDataType::String, SemanticType::Field);
@@ -144,6 +147,25 @@ pub const LATEST_OFFSET_COLUMN: EventTableColumn =
pub const GC_REPORT_COLUMN: EventTableColumn =
EventTableColumn::json_binary("gc_report", ColumnDataType::Binary, SemanticType::Field);
+/// The canonical ADMIN function name field.
+pub const ADMIN_FUNCTION_NAME_COLUMN: EventTableColumn = EventTableColumn::new(
+ "admin_function_name",
+ ColumnDataType::String,
+ SemanticType::Field,
+);
+/// The canonical ADMIN function execution status field.
+pub const ADMIN_FUNCTION_STATUS_COLUMN: EventTableColumn = EventTableColumn::new(
+ "admin_function_status",
+ ColumnDataType::String,
+ SemanticType::Field,
+);
+/// The canonical ADMIN function output field.
+pub const ADMIN_FUNCTION_OUTPUT_COLUMN: EventTableColumn = EventTableColumn::json_binary(
+ "admin_function_output",
+ ColumnDataType::Binary,
+ SemanticType::Field,
+);
+
/// The canonical table name field for table DDL events.
pub const TABLE_NAME_COLUMN: EventTableColumn =
EventTableColumn::new("table_name", ColumnDataType::String, SemanticType::Field);
@@ -389,6 +411,47 @@ mod tests {
);
}
+ #[test]
+ fn admin_function_schema_preserves_names_types_semantics_extensions_and_order() {
+ assert_eq!(
+ column_schemas([
+ &ACTOR_COLUMN,
+ &ADMIN_FUNCTION_NAME_COLUMN,
+ &ADMIN_FUNCTION_STATUS_COLUMN,
+ &ADMIN_FUNCTION_OUTPUT_COLUMN,
+ ]),
+ vec![
+ ColumnSchema {
+ column_name: "actor".to_string(),
+ datatype: ColumnDataType::String.into(),
+ semantic_type: SemanticType::Field.into(),
+ ..Default::default()
+ },
+ ColumnSchema {
+ column_name: "admin_function_name".to_string(),
+ datatype: ColumnDataType::String.into(),
+ semantic_type: SemanticType::Field.into(),
+ ..Default::default()
+ },
+ ColumnSchema {
+ column_name: "admin_function_status".to_string(),
+ datatype: ColumnDataType::String.into(),
+ semantic_type: SemanticType::Field.into(),
+ ..Default::default()
+ },
+ ColumnSchema {
+ column_name: "admin_function_output".to_string(),
+ datatype: ColumnDataType::Binary.into(),
+ semantic_type: SemanticType::Field.into(),
+ datatype_extension: Some(ColumnDataTypeExtension {
+ type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
+ }),
+ ..Default::default()
+ },
+ ]
+ );
+ }
+
#[test]
fn table_dimension_schema_preserves_names_types_semantics_and_order() {
assert_eq!(
diff --git a/src/frontend/src/frontend.rs b/src/frontend/src/frontend.rs
index 30fcd394922..17435c01a14 100644
--- a/src/frontend/src/frontend.rs
+++ b/src/frontend/src/frontend.rs
@@ -116,7 +116,11 @@ impl Default for FrontendOptions {
impl Configurable for FrontendOptions {
fn env_list_keys() -> Option<&'static [&'static str]> {
- Some(&["heartbeat_env_vars", "meta_client.metasrv_addrs"])
+ Some(&[
+ "heartbeat_env_vars",
+ "meta_client.metasrv_addrs",
+ "event_recorder.event_types",
+ ])
}
}
diff --git a/src/frontend/src/instance/builder.rs b/src/frontend/src/instance/builder.rs
index 42d234fae41..b8e3a9f2828 100644
--- a/src/frontend/src/instance/builder.rs
+++ b/src/frontend/src/instance/builder.rs
@@ -20,7 +20,7 @@ use catalog::CatalogManagerRef;
use catalog::process_manager::ProcessManagerRef;
use common_base::Plugins;
use common_datasource::object_store::LocalFileAccess;
-use common_event_recorder::EventRecorderImpl;
+use common_event_recorder::{EventRecorderImpl, EventRecorderRef};
use common_meta::cache::{LayeredCacheRegistryRef, TableRouteCacheRef};
use common_meta::cache_invalidator::{CacheInvalidatorRef, DummyCacheInvalidator};
use common_meta::key::TableMetadataManager;
@@ -37,8 +37,8 @@ use operator::request::Requester;
#[cfg(feature = "enterprise")]
use operator::statement::CreateDatabaseHandlerRef;
use operator::statement::{
- ExecutorConfigureContext, StatementExecutor, StatementExecutorConfiguratorRef,
- StatementExecutorRef,
+ AdminEventRecorderHandle, AdminFunctionRecordingLayer, ExecutorConfigureContext,
+ StatementExecutor, StatementExecutorConfiguratorRef, StatementExecutorRef,
};
use operator::table::TableMutationOperator;
use partition::cache::PartitionInfoCacheRef;
@@ -302,6 +302,10 @@ impl FrontendBuilder {
statement_executor
};
+ let admin_event_recorder = AdminEventRecorderHandle::default();
+ let statement_executor = statement_executor.with_admin_function_layer(Arc::new(
+ AdminFunctionRecordingLayer::new(admin_event_recorder.clone()),
+ ));
let statement_executor = Arc::new(statement_executor);
let pipeline_operator = Arc::new(PipelineOperator::new(
@@ -316,13 +320,14 @@ impl FrontendBuilder {
let slow_query_recorder = Arc::new(EventRecorderImpl::new(Box::new(
EventHandlerImpl::new(statement_executor.clone(), self.options.slow_query.ttl),
)));
- let event_recorder = Arc::new(EventRecorderImpl::with_event_type_filter(
+ let event_recorder: EventRecorderRef = Arc::new(EventRecorderImpl::with_event_type_filter(
Box::new(EventHandlerImpl::new(
statement_executor.clone(),
self.options.event_recorder.ttl,
)),
self.options.event_recorder.event_types.clone(),
));
+ admin_event_recorder.install(&event_recorder);
Ok(Instance {
frontend_peer_addr,
diff --git a/src/operator/Cargo.toml b/src/operator/Cargo.toml
index bd34e21ed24..087ea461f10 100644
--- a/src/operator/Cargo.toml
+++ b/src/operator/Cargo.toml
@@ -33,6 +33,7 @@ common-base.workspace = true
common-catalog.workspace = true
common-datasource.workspace = true
common-error.workspace = true
+common-event-recorder.workspace = true
common-frontend.workspace = true
common-function.workspace = true
common-grpc.workspace = true
diff --git a/src/operator/src/error.rs b/src/operator/src/error.rs
index eabd6477737..244776ddc9d 100644
--- a/src/operator/src/error.rs
+++ b/src/operator/src/error.rs
@@ -65,6 +65,9 @@ pub enum Error {
location: Location,
},
+ #[snafu(display("Admin function execution was cancelled"))]
+ AdminFunctionCancelled,
+
#[snafu(display("Expected {expected} args, but actual {actual}"))]
FunctionArityMismatch { expected: usize, actual: usize },
@@ -997,6 +1000,7 @@ impl ErrorExt for Error {
| Error::EncodeJson { .. }
| Error::DeserializePartitionExpr { .. }
| Error::SerializePartitionExpr { .. } => StatusCode::Unexpected,
+ Error::AdminFunctionCancelled => StatusCode::Cancelled,
Error::ViewNotFound { .. }
| Error::ViewInfoNotFound { .. }
| Error::TableNotFound { .. } => StatusCode::TableNotFound,
diff --git a/src/operator/src/statement.rs b/src/operator/src/statement.rs
index 320e7c2e642..1143b1c68ce 100644
--- a/src/operator/src/statement.rs
+++ b/src/operator/src/statement.rs
@@ -80,6 +80,11 @@ use table::requests::{CopyDatabaseRequest, CopyDirection, CopyQueryToRequest, Co
use table::table_name::TableName;
use table::table_reference::TableReference;
+pub use self::admin::{
+ AdminEventRecorderHandle, AdminFunctionLayer, AdminFunctionLayerRef,
+ AdminFunctionRecordingLayer, AdminFunctionRequest, AdminFunctionResponse, AdminFunctionService,
+ AdminFunctionServiceRef,
+};
use self::set::{
set_bytea_output, set_datestyle, set_intervalstyle, set_timezone, validate_client_encoding,
};
@@ -138,6 +143,7 @@ pub struct StatementExecutor {
inserter: InserterRef,
process_manager: Option,
origin_frontend_addr: String,
+ admin_function_service: AdminFunctionServiceRef,
pub(crate) local_file_access: LocalFileAccess,
#[cfg(feature = "enterprise")]
create_database_handler: Option,
@@ -179,6 +185,7 @@ impl StatementExecutor {
origin_frontend_addr: String,
local_file_access: LocalFileAccess,
) -> Self {
+ let admin_function_service = admin::new_admin_function_service(query_engine.clone());
Self {
catalog_manager,
query_engine,
@@ -191,6 +198,7 @@ impl StatementExecutor {
inserter,
process_manager,
origin_frontend_addr,
+ admin_function_service,
local_file_access,
#[cfg(feature = "enterprise")]
create_database_handler: None,
@@ -199,6 +207,14 @@ impl StatementExecutor {
}
}
+ /// Adds a layer around the ADMIN function execution service.
+ ///
+ /// The last added layer is the outermost layer.
+ pub fn with_admin_function_layer(mut self, layer: AdminFunctionLayerRef) -> Self {
+ self.admin_function_service = layer.layer(self.admin_function_service);
+ self
+ }
+
#[cfg(feature = "enterprise")]
pub fn with_trigger_querier(mut self, querier: TriggerQuerierRef) -> Self {
self.trigger_querier = Some(querier);
diff --git a/src/operator/src/statement/admin.rs b/src/operator/src/statement/admin.rs
index 7494977b214..2b4a909e3cd 100644
--- a/src/operator/src/statement/admin.rs
+++ b/src/operator/src/statement/admin.rs
@@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+mod event;
+mod layer;
+
use std::sync::Arc;
use common_function::function::FunctionContext;
@@ -28,6 +31,10 @@ use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::value::Value;
use datatypes::vectors::VectorRef;
+pub use layer::{
+ AdminEventRecorderHandle, AdminFunctionLayer, AdminFunctionLayerRef,
+ AdminFunctionRecordingLayer,
+};
use session::context::QueryContextRef;
use snafu::{OptionExt, ResultExt, ensure};
use sql::ast::{Expr, FunctionArg, FunctionArgExpr, FunctionArguments, Value as SqlValue};
@@ -38,14 +45,49 @@ use crate::statement::StatementExecutor;
const DUMMY_COLUMN: &str = "";
-impl StatementExecutor {
- /// Execute the [`Admin`] statement and returns the output.
- #[tracing::instrument(skip_all)]
- pub(super) async fn execute_admin_command(
- &self,
- stmt: Admin,
- query_ctx: QueryContextRef,
- ) -> Result