From 57e49e67379835607cb5f62fc84c96e56530da69 Mon Sep 17 00:00:00 2001 From: evan slack <51209817+evanofslack@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:19:20 -0500 Subject: [PATCH] feat(obs): Add metric to count all s3 operations (#2088) Co-authored-by: loverustfs Co-authored-by: houseme --- rustfs/src/app/bucket_usecase.rs | 3 - rustfs/src/storage/ecfs.rs | 74 ++++++++++++- rustfs/src/storage/mod.rs | 1 + rustfs/src/storage/s3_metrics.rs | 174 +++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 rustfs/src/storage/s3_metrics.rs diff --git a/rustfs/src/app/bucket_usecase.rs b/rustfs/src/app/bucket_usecase.rs index baff520a1..16b519413 100644 --- a/rustfs/src/app/bucket_usecase.rs +++ b/rustfs/src/app/bucket_usecase.rs @@ -25,7 +25,6 @@ use crate::storage::s3_api::{acl, encryption, replication, tagging}; use crate::storage::*; use futures::StreamExt; use http::StatusCode; -use metrics::counter; use rustfs_config::RUSTFS_REGION; use rustfs_ecstore::bucket::{ lifecycle::bucket_lifecycle_ops::validate_transition_tier, @@ -163,8 +162,6 @@ impl DefaultBucketUsecase { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; - counter!("rustfs_create_bucket_total").increment(1); - let make_result = store .make_bucket( &bucket, diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index e8a33eac1..fc86a513c 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -15,6 +15,7 @@ use crate::app::bucket_usecase::DefaultBucketUsecase; use crate::app::multipart_usecase::DefaultMultipartUsecase; use crate::app::object_usecase::DefaultObjectUsecase; +use crate::storage::s3_metrics::{S3Operation, record_s3_op}; use rustfs_ecstore::{ bucket::tagging::decode_tags_to_map, error::{is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found}, @@ -97,7 +98,7 @@ impl AsyncSeek for InMemoryAsyncReader { impl FS { pub fn new() -> Self { - // let store: ECStore = ECStore::new(address, endpoint_pools).await?; + crate::storage::s3_metrics::init_s3_metrics(); Self {} } @@ -189,6 +190,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::AbortMultipartUpload, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_abort_multipart_upload(req).await } @@ -198,6 +200,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::CompleteMultipartUpload, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_complete_multipart_upload(req).await } @@ -205,6 +208,7 @@ impl S3 for FS { /// Copy an object from one location to another #[instrument(level = "debug", skip(self, req))] async fn copy_object(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::CopyObject, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_copy_object(req).await } @@ -215,6 +219,7 @@ impl S3 for FS { fields(start_time=?time::OffsetDateTime::now_utc()) )] async fn create_bucket(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::CreateBucket, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_create_bucket(req).await } @@ -224,6 +229,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::CreateMultipartUpload, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_create_multipart_upload(req).await } @@ -231,12 +237,14 @@ impl S3 for FS { /// Delete a bucket #[instrument(level = "debug", skip(self, req))] async fn delete_bucket(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::DeleteBucket, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket(req).await } #[instrument(level = "debug", skip(self))] async fn delete_bucket_cors(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::DeleteBucketCors, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket_cors(req).await } @@ -245,6 +253,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeleteBucketEncryption, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket_encryption(req).await } @@ -254,6 +263,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeleteBucketLifecycle, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket_lifecycle(req).await } @@ -262,6 +272,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeleteBucketPolicy, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket_policy(req).await } @@ -270,6 +281,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeleteBucketReplication, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket_replication(req).await } @@ -279,6 +291,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeleteBucketTagging, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_bucket_tagging(req).await } @@ -288,6 +301,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeletePublicAccessBlock, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_delete_public_access_block(req).await } @@ -295,6 +309,7 @@ impl S3 for FS { /// Delete an object #[instrument(level = "debug", skip(self, req))] async fn delete_object(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::DeleteObject, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_delete_object(req).await } @@ -304,6 +319,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::DeleteObjectTagging, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_delete_object_tagging(req).await } @@ -311,17 +327,20 @@ impl S3 for FS { /// Delete multiple objects #[instrument(level = "debug", skip(self, req))] async fn delete_objects(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::DeleteObjects, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_delete_objects(req).await } async fn get_bucket_acl(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetBucketAcl, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_acl(req).await } #[instrument(level = "debug", skip(self))] async fn get_bucket_cors(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetBucketCors, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_cors(req).await } @@ -330,6 +349,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetBucketEncryption, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_encryption(req).await } @@ -339,6 +359,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetBucketLifecycleConfiguration, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_lifecycle_configuration(req).await } @@ -346,6 +367,7 @@ impl S3 for FS { /// Get bucket location #[instrument(level = "debug", skip(self, req))] async fn get_bucket_location(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetBucketLocation, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_location(req).await } @@ -354,11 +376,13 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetBucketNotificationConfiguration, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_notification_configuration(req).await } async fn get_bucket_policy(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetBucketPolicy, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_policy(req).await } @@ -367,6 +391,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetBucketPolicyStatus, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_policy_status(req).await } @@ -375,12 +400,14 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetBucketReplication, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_replication(req).await } #[instrument(level = "debug", skip(self))] async fn get_bucket_tagging(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetBucketTagging, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_tagging(req).await } @@ -390,6 +417,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetPublicAccessBlock, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_public_access_block(req).await } @@ -399,6 +427,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetBucketVersioning, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_get_bucket_versioning(req).await } @@ -410,11 +439,13 @@ impl S3 for FS { fields(start_time=?time::OffsetDateTime::now_utc()) )] async fn get_object(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetObject, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object(req).await } async fn get_object_acl(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetObjectAcl, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object_acl(req).await } @@ -423,6 +454,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetObjectAttributes, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object_attributes(req).await } @@ -431,6 +463,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetObjectLegalHold, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object_legal_hold(req).await } @@ -440,6 +473,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetObjectLockConfiguration, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object_lock_configuration(req).await } @@ -448,38 +482,45 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::GetObjectRetention, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object_retention(req).await } #[instrument(level = "debug", skip(self))] async fn get_object_tagging(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetObjectTagging, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_get_object_tagging(req).await } - #[instrument(level = "debug", skip(self, _req))] - async fn get_object_torrent(&self, _req: S3Request) -> S3Result> { + #[instrument(level = "debug", skip(self, req))] + async fn get_object_torrent(&self, req: S3Request) -> S3Result> { // Torrent functionality is not implemented in RustFS // Per S3 API test expectations, return 404 NoSuchKey (not 501 Not Implemented) // This allows clients to gracefully handle the absence of torrent support + record_s3_op(S3Operation::GetObjectTorrent, &req.input.bucket); Err(S3Error::new(S3ErrorCode::NoSuchKey)) } #[instrument(level = "debug", skip(self, req))] async fn head_bucket(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::HeadBucket, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_head_bucket(req).await } #[instrument(level = "debug", skip(self, req))] async fn head_object(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::HeadObject, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_head_object(req).await } #[instrument(level = "debug", skip(self))] async fn list_buckets(&self, req: S3Request) -> S3Result> { + // List buckets not associated with a bucket, give it bucket label "*" to denote "all". + record_s3_op(S3Operation::ListBuckets, "*"); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_list_buckets(req).await } @@ -488,6 +529,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::ListMultipartUploads, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_list_multipart_uploads(req).await } @@ -496,40 +538,47 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::ListObjectVersions, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_list_object_versions(req).await } #[instrument(level = "debug", skip(self, req))] async fn list_objects(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::ListObjects, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_list_objects(req).await } #[instrument(level = "debug", skip(self, req))] async fn list_objects_v2(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::ListObjectsV2, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_list_objects_v2(req).await } #[instrument(level = "debug", skip(self, req))] async fn list_parts(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::ListParts, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_list_parts(req).await } async fn put_bucket_acl(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutBucketAcl, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_acl(req).await } #[instrument(level = "debug", skip(self))] async fn put_bucket_cors(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutBucketCors, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_cors(req).await } async fn get_bucket_logging(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::GetBucketLogging, &req.input.bucket); let Some(store) = new_object_layer_fn() else { return Err(s3_error!(InternalError, "Not init")); }; @@ -541,6 +590,7 @@ impl S3 for FS { } async fn put_bucket_logging(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutBucketLogging, &req.input.bucket); let Some(store) = new_object_layer_fn() else { return Err(s3_error!(InternalError, "Not init")); }; @@ -555,6 +605,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutBucketEncryption, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_encryption(req).await } @@ -564,6 +615,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutBucketLifecycleConfiguration, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_lifecycle_configuration(req).await } @@ -572,11 +624,13 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutBucketNotificationConfiguration, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_notification_configuration(req).await } async fn put_bucket_policy(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutBucketPolicy, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_policy(req).await } @@ -585,6 +639,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutBucketReplication, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_replication(req).await } @@ -594,12 +649,14 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutPublicAccessBlock, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_public_access_block(req).await } #[instrument(level = "debug", skip(self))] async fn put_bucket_tagging(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutBucketTagging, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_tagging(req).await } @@ -609,17 +666,20 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutBucketVersioning, &req.input.bucket); let usecase = DefaultBucketUsecase::from_global(); usecase.execute_put_bucket_versioning(req).await } #[instrument(level = "debug", skip(self, req))] async fn put_object(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutObject, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_put_object(self, req).await } async fn put_object_acl(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutObjectAcl, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_put_object_acl(req).await } @@ -628,6 +688,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutObjectLegalHold, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_put_object_legal_hold(req).await } @@ -637,6 +698,7 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutObjectLockConfiguration, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_put_object_lock_configuration(req).await } @@ -645,17 +707,20 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::PutObjectRetention, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_put_object_retention(req).await } #[instrument(level = "debug", skip(self, req))] async fn put_object_tagging(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::PutObjectTagging, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_put_object_tagging(req).await } async fn restore_object(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::RestoreObject, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_restore_object(req).await } @@ -664,18 +729,21 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { + record_s3_op(S3Operation::SelectObjectContent, &req.input.bucket); let usecase = DefaultObjectUsecase::from_global(); usecase.execute_select_object_content(req).await } #[instrument(level = "debug", skip(self, req))] async fn upload_part(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::UploadPart, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_upload_part(req).await } #[instrument(level = "debug", skip(self, req))] async fn upload_part_copy(&self, req: S3Request) -> S3Result> { + record_s3_op(S3Operation::UploadPartCopy, &req.input.bucket); let usecase = DefaultMultipartUsecase::from_global(); usecase.execute_upload_part_copy(req).await } diff --git a/rustfs/src/storage/mod.rs b/rustfs/src/storage/mod.rs index 49aab9913..33af8138a 100644 --- a/rustfs/src/storage/mod.rs +++ b/rustfs/src/storage/mod.rs @@ -21,6 +21,7 @@ pub mod options; pub(crate) mod readers; pub mod rpc; pub(crate) mod s3_api; +pub(crate) mod s3_metrics; mod sse; pub mod tonic_service; diff --git a/rustfs/src/storage/s3_metrics.rs b/rustfs/src/storage/s3_metrics.rs new file mode 100644 index 000000000..c4ca5fcd5 --- /dev/null +++ b/rustfs/src/storage/s3_metrics.rs @@ -0,0 +1,174 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use metrics::{counter, describe_counter}; +use std::sync::OnceLock; + +const S3_OPS_METRIC: &str = "rustfs_s3_operations_total"; + +#[derive(Clone, Copy)] +pub enum S3Operation { + AbortMultipartUpload, + CompleteMultipartUpload, + CopyObject, + CreateBucket, + CreateMultipartUpload, + DeleteBucket, + DeleteBucketCors, + DeleteBucketEncryption, + DeleteBucketLifecycle, + DeleteBucketPolicy, + DeleteBucketReplication, + DeleteBucketTagging, + DeleteObject, + DeleteObjectTagging, + DeleteObjects, + DeletePublicAccessBlock, + GetBucketAcl, + GetBucketCors, + GetBucketEncryption, + GetBucketLifecycleConfiguration, + GetBucketLocation, + GetBucketLogging, + GetBucketNotificationConfiguration, + GetBucketPolicy, + GetBucketPolicyStatus, + GetBucketReplication, + GetBucketTagging, + GetBucketVersioning, + GetObject, + GetObjectAcl, + GetObjectAttributes, + GetObjectLegalHold, + GetObjectLockConfiguration, + GetObjectRetention, + GetObjectTagging, + GetObjectTorrent, + GetPublicAccessBlock, + HeadBucket, + HeadObject, + ListBuckets, + ListMultipartUploads, + ListObjectVersions, + ListObjects, + ListObjectsV2, + ListParts, + PutBucketAcl, + PutBucketCors, + PutBucketEncryption, + PutBucketLifecycleConfiguration, + PutBucketLogging, + PutBucketNotificationConfiguration, + PutBucketPolicy, + PutBucketReplication, + PutBucketTagging, + PutBucketVersioning, + PutObject, + PutObjectAcl, + PutObjectLegalHold, + PutObjectLockConfiguration, + PutObjectRetention, + PutObjectTagging, + PutPublicAccessBlock, + RestoreObject, + SelectObjectContent, + UploadPart, + UploadPartCopy, +} + +impl S3Operation { + pub fn as_str(self) -> &'static str { + match self { + Self::AbortMultipartUpload => "AbortMultipartUpload", + Self::CompleteMultipartUpload => "CompleteMultipartUpload", + Self::CopyObject => "CopyObject", + Self::CreateBucket => "CreateBucket", + Self::CreateMultipartUpload => "CreateMultipartUpload", + Self::DeleteBucket => "DeleteBucket", + Self::DeleteBucketCors => "DeleteBucketCors", + Self::DeleteBucketEncryption => "DeleteBucketEncryption", + Self::DeleteBucketLifecycle => "DeleteBucketLifecycle", + Self::DeleteBucketPolicy => "DeleteBucketPolicy", + Self::DeleteBucketReplication => "DeleteBucketReplication", + Self::DeleteBucketTagging => "DeleteBucketTagging", + Self::DeleteObject => "DeleteObject", + Self::DeleteObjectTagging => "DeleteObjectTagging", + Self::DeleteObjects => "DeleteObjects", + Self::DeletePublicAccessBlock => "DeletePublicAccessBlock", + Self::GetBucketAcl => "GetBucketAcl", + Self::GetBucketCors => "GetBucketCors", + Self::GetBucketEncryption => "GetBucketEncryption", + Self::GetBucketLifecycleConfiguration => "GetBucketLifecycleConfiguration", + Self::GetBucketLocation => "GetBucketLocation", + Self::GetBucketLogging => "GetBucketLogging", + Self::GetBucketNotificationConfiguration => "GetBucketNotificationConfiguration", + Self::GetBucketPolicy => "GetBucketPolicy", + Self::GetBucketPolicyStatus => "GetBucketPolicyStatus", + Self::GetBucketReplication => "GetBucketReplication", + Self::GetBucketTagging => "GetBucketTagging", + Self::GetBucketVersioning => "GetBucketVersioning", + Self::GetObject => "GetObject", + Self::GetObjectAcl => "GetObjectAcl", + Self::GetObjectAttributes => "GetObjectAttributes", + Self::GetObjectLegalHold => "GetObjectLegalHold", + Self::GetObjectLockConfiguration => "GetObjectLockConfiguration", + Self::GetObjectRetention => "GetObjectRetention", + Self::GetObjectTagging => "GetObjectTagging", + Self::GetObjectTorrent => "GetObjectTorrent", + Self::GetPublicAccessBlock => "GetPublicAccessBlock", + Self::HeadBucket => "HeadBucket", + Self::HeadObject => "HeadObject", + Self::ListBuckets => "ListBuckets", + Self::ListMultipartUploads => "ListMultipartUploads", + Self::ListObjectVersions => "ListObjectVersions", + Self::ListObjects => "ListObjects", + Self::ListObjectsV2 => "ListObjectsV2", + Self::ListParts => "ListParts", + Self::PutBucketAcl => "PutBucketAcl", + Self::PutBucketCors => "PutBucketCors", + Self::PutBucketEncryption => "PutBucketEncryption", + Self::PutBucketLifecycleConfiguration => "PutBucketLifecycleConfiguration", + Self::PutBucketLogging => "PutBucketLogging", + Self::PutBucketNotificationConfiguration => "PutBucketNotificationConfiguration", + Self::PutBucketPolicy => "PutBucketPolicy", + Self::PutBucketReplication => "PutBucketReplication", + Self::PutBucketTagging => "PutBucketTagging", + Self::PutBucketVersioning => "PutBucketVersioning", + Self::PutObject => "PutObject", + Self::PutObjectAcl => "PutObjectAcl", + Self::PutObjectLegalHold => "PutObjectLegalHold", + Self::PutObjectLockConfiguration => "PutObjectLockConfiguration", + Self::PutObjectRetention => "PutObjectRetention", + Self::PutObjectTagging => "PutObjectTagging", + Self::PutPublicAccessBlock => "PutPublicAccessBlock", + Self::RestoreObject => "RestoreObject", + Self::SelectObjectContent => "SelectObjectContent", + Self::UploadPart => "UploadPart", + Self::UploadPartCopy => "UploadPartCopy", + } + } +} + +pub fn record_s3_op(op: S3Operation, bucket: &str) { + counter!(S3_OPS_METRIC, "op" => op.as_str(), "bucket" => bucket.to_owned()).increment(1); +} + +/// One-time registration of indicator meta information +/// This function ensures that metric descriptors are registered only once. +pub fn init_s3_metrics() { + static METRICS_DESC_INIT: OnceLock<()> = OnceLock::new(); + METRICS_DESC_INIT.get_or_init(|| { + describe_counter!(S3_OPS_METRIC, "Total number of S3 API operations handled"); + }); +}