From 03589201fb23e03a3bd7e5250f656b64bfcc7520 Mon Sep 17 00:00:00 2001 From: houseme Date: Fri, 21 Feb 2025 13:46:35 +0800 Subject: [PATCH 01/27] Implement initialization for packages/logging - Add initialization logic for the `rustfs-logging` crate. - Provide examples for logging utilities. - Include tests for logging and telemetry functionalities. - Ensure proper configuration of dependencies in `Cargo.toml`. --- Cargo.lock | 140 +++++++++++++++++++++++++++ Cargo.toml | 10 +- packages/logging/Cargo.toml | 35 +++++++ packages/logging/src/audit.rs | 153 ++++++++++++++++++++++++++++++ packages/logging/src/lib.rs | 83 ++++++++++++++++ packages/logging/src/logger.rs | 46 +++++++++ packages/logging/src/telemetry.rs | 113 ++++++++++++++++++++++ rustfs/src/logging/mod.rs | 0 rustfs/src/main.rs | 14 +-- 9 files changed, 587 insertions(+), 7 deletions(-) create mode 100644 packages/logging/Cargo.toml create mode 100644 packages/logging/src/audit.rs create mode 100644 packages/logging/src/lib.rs create mode 100644 packages/logging/src/logger.rs create mode 100644 packages/logging/src/telemetry.rs create mode 100644 rustfs/src/logging/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d1dc017d0..d6a65c06e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4290,6 +4290,109 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "opentelemetry" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.11", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", + "tracing", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.11", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb3a2f78c2d55362cd6c313b8abedfbc0142ab3c2676822068fd2ab7d51f9b7" + +[[package]] +name = "opentelemetry-stdout" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb0e5a5132e4b80bf037a78e3e12c8402535199f5de490d0c38f7eac71bc831" +dependencies = [ + "async-trait", + "chrono", + "futures-util", + "opentelemetry", + "opentelemetry_sdk", + "serde", + "thiserror 2.0.11", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror 2.0.11", + "tokio", + "tokio-stream", + "tracing", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -5213,6 +5316,7 @@ dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -5443,6 +5547,23 @@ dependencies = [ "zip", ] +[[package]] +name = "rustfs-logging" +version = "0.0.1" +dependencies = [ + "chrono", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry-stdout", + "opentelemetry_sdk", + "serde", + "tokio", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", +] + [[package]] name = "rustix" version = "0.38.44" @@ -6440,6 +6561,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -6738,6 +6860,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index eed309921..511155ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "iam", "crypto", "cli/rustfs-gui", + "packages/logging", ] resolver = "2" @@ -59,6 +60,11 @@ lock = { path = "./common/lock" } lazy_static = "1.5.0" mime = "0.3.17" netif = "0.1.6" +opentelemetry = { version = "0.28" } +opentelemetry_sdk = { version = "0.28" } +opentelemetry-stdout = { version = "0.28.0" } +opentelemetry-otlp = { version = "0.28" } +opentelemetry-semantic-conventions = { version = "0.28.0", features = ["semconv_experimental"] } pin-project-lite = "0.2" # pin-utils = "0.1.0" prost = "0.13.4" @@ -71,6 +77,7 @@ reqwest = { version = "0.12.12", default-features = false, features = ["rustls-t rfd = "0.15.2" rmp = "0.8.14" rmp-serde = "1.3.0" +rustfs-logging = { path = "packages/logging", version = "0.0.1" } s3s = { git = "https://github.com/Nugine/s3s.git", rev = "529c8933a11528c506d5fbf7c4c2ab155db37dfe", default-features = true, features = [ "tower", ] } @@ -96,6 +103,7 @@ tracing = "0.1.41" tracing-error = "0.2.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] } tracing-appender = "0.2.3" +tracing-opentelemetry = "0.29" transform-stream = "0.3.1" url = "2.5.4" uuid = { version = "1.12.1", features = [ @@ -108,4 +116,4 @@ axum = "0.7.9" md-5 = "0.10.6" workers = { path = "./common/workers" } test-case = "3.3.1" -zip = "2.2.2" \ No newline at end of file +zip = "2.2.2" diff --git a/packages/logging/Cargo.toml b/packages/logging/Cargo.toml new file mode 100644 index 000000000..af37740d9 --- /dev/null +++ b/packages/logging/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "rustfs-logging" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +opentelemetry = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } +opentelemetry-stdout = { workspace = true } +opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "metrics"] } +opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } +serde = { workspace = true } +tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } +tokio = { workspace = true, features = ["full"] } + + + +[dev-dependencies] +chrono = { workspace = true } +opentelemetry = { workspace = true, features = ["trace", "metrics"] } +opentelemetry_sdk = { workspace = true, features = ["trace", "rt-tokio"] } +opentelemetry-stdout = { workspace = true, features = ["trace", "metrics"] } +opentelemetry-otlp = { workspace = true, features = ["metrics", "grpc-tonic"] } +opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true, features = ["std", "attributes"] } +tracing-subscriber = { workspace = true, features = ["registry", "std", "fmt"] } \ No newline at end of file diff --git a/packages/logging/src/audit.rs b/packages/logging/src/audit.rs new file mode 100644 index 000000000..0f120db2c --- /dev/null +++ b/packages/logging/src/audit.rs @@ -0,0 +1,153 @@ +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc; + +/// AuditEntry is a struct that represents an audit entry +/// that can be logged +/// # Fields +/// * `version` - The version of the audit entry +/// * `event_type` - The type of event that occurred +/// * `bucket` - The bucket that was accessed +/// * `object` - The object that was accessed +/// * `user` - The user that accessed the object +/// * `time` - The time the event occurred +/// * `user_agent` - The user agent that accessed the object +/// * `span_id` - The span ID of the event +/// # Example +/// ``` +/// use rustfs_logging::AuditEntry; +/// let entry = AuditEntry { +/// version: "1.0".to_string(), +/// event_type: "read".to_string(), +/// bucket: "bucket".to_string(), +/// object: "object".to_string(), +/// user: "user".to_string(), +/// time: "time".to_string(), +/// user_agent: "user_agent".to_string(), +/// span_id: "span_id".to_string(), +/// }; +/// ``` +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AuditEntry { + pub version: String, + pub event_type: String, + pub bucket: String, + pub object: String, + pub user: String, + pub time: String, + pub user_agent: String, + pub span_id: String, +} + +/// AuditTarget is a trait that defines the interface for audit targets +/// that can receive audit entries +pub trait AuditTarget: Send + Sync { + fn send(&self, entry: AuditEntry); +} + +/// FileAuditTarget is an audit target that logs audit entries to a file +pub struct FileAuditTarget; + +impl AuditTarget for FileAuditTarget { + /// Send an audit entry to a file + /// # Arguments + /// * `entry` - The audit entry to send + /// # Example + /// ``` + /// use rustfs_logging::{AuditEntry, AuditTarget, FileAuditTarget}; + /// let entry = AuditEntry { + /// version: "1.0".to_string(), + /// event_type: "read".to_string(), + /// bucket: "bucket".to_string(), + /// object: "object".to_string(), + /// user: "user".to_string(), + /// time: "time".to_string(), + /// user_agent: "user_agent".to_string(), + /// span_id: "span_id".to_string(), + /// }; + /// FileAuditTarget.send(entry); + /// ``` + /// + fn send(&self, entry: AuditEntry) { + println!("File audit: {:?}", entry); + } +} + +/// AuditLogger is a logger that logs audit entries +/// to multiple targets +/// +/// # Example +/// ``` +/// use rustfs_logging::{AuditEntry, AuditLogger, FileAuditTarget}; +/// +/// #[tokio::main] +/// async fn main() { +/// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); +/// let entry = AuditEntry { +/// version: "1.0".to_string(), +/// event_type: "read".to_string(), +/// bucket: "bucket".to_string(), +/// object: "object".to_string(), +/// user: "user".to_string(), +/// time: "time".to_string(), +/// user_agent: "user_agent".to_string(), +/// span_id: "span_id".to_string(), +/// }; +/// logger.log(entry).await; +/// } +/// ``` +#[derive(Debug)] +pub struct AuditLogger { + tx: mpsc::Sender, +} + +impl AuditLogger { + /// Create a new AuditLogger with the given targets + /// that will receive audit entries + /// # Arguments + /// * `targets` - A vector of audit targets + /// # Returns + /// * An AuditLogger + /// # Example + /// ``` + /// use rustfs_logging::{AuditLogger, AuditEntry, FileAuditTarget}; + /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); + /// ``` + pub fn new(targets: Vec>) -> Self { + let (tx, mut rx) = mpsc::channel::(1000); + tokio::spawn(async move { + while let Some(entry) = rx.recv().await { + for target in &targets { + target.send(entry.clone()); + } + } + }); + Self { tx } + } + + /// Log an audit entry + /// # Arguments + /// * `entry` - The audit entry to log + /// # Example + /// ``` + /// use rustfs_logging::{AuditEntry, AuditLogger, FileAuditTarget}; + /// + /// #[tokio::main] + /// async fn main() { + /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); + /// let entry = AuditEntry { + /// version: "1.0".to_string(), + /// event_type: "read".to_string(), + /// bucket: "bucket".to_string(), + /// object: "object".to_string(), + /// user: "user".to_string(), + /// time: "time".to_string(), + /// user_agent: "user_agent".to_string(), + /// span_id: "span_id".to_string(), + /// }; + /// logger.log(entry).await; + /// } + /// ``` + pub async fn log(&self, entry: AuditEntry) { + let _ = self.tx.send(entry).await; + } +} diff --git a/packages/logging/src/lib.rs b/packages/logging/src/lib.rs new file mode 100644 index 000000000..3fdbb83d8 --- /dev/null +++ b/packages/logging/src/lib.rs @@ -0,0 +1,83 @@ +//! Logging utilities +/// +/// This crate provides utilities for logging. +/// +/// # Examples +/// ``` +/// use rustfs_logging::{log_info, log_error}; +/// +/// log_info("This is an informational message"); +/// log_error("This is an error message"); +/// ``` +pub use audit::{AuditEntry, AuditLogger, AuditTarget, FileAuditTarget}; +pub use logger::{log_debug, log_error, log_info}; +pub use telemetry::Telemetry; + +mod audit; +mod logger; +mod telemetry; + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry::global; + use opentelemetry::trace::TraceContextExt; + use std::time::{Duration, SystemTime}; + use tracing::{instrument, Span}; + use tracing_opentelemetry::OpenTelemetrySpanExt; + + #[instrument(fields(bucket, object, user))] + async fn put_object(audit_logger: &AuditLogger, bucket: String, object: String, user: String) { + let start_time = SystemTime::now(); + log_info("Starting PUT operation"); + + // Simulate the operation + tokio::time::sleep(Duration::from_millis(100)).await; + + // Record Metrics + let meter = global::meter("rustfs.rs"); + let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); + request_duration.record( + start_time.elapsed().unwrap().as_secs_f64(), + &[opentelemetry::KeyValue::new("operation", "put_object")], + ); + + // Gets the current span + let span = Span::current(); + + // Use 'OpenTelemetrySpanExt' to get 'SpanContext' + let span_context = span.context(); // Get context via OpenTelemetrySpanExt + let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId + + // Audit events are logged + let audit_entry = AuditEntry { + version: "1.0".to_string(), + event_type: "s3_put_object".to_string(), + bucket, + object, + user, + time: chrono::Utc::now().to_rfc3339(), + user_agent: "rustfs.rs-client".to_string(), + span_id, + }; + audit_logger.log(audit_entry).await; + + log_info("PUT operation completed"); + } + + #[tokio::test] + async fn test_main() { + let telemetry = Telemetry::init(); + + // Initialize multiple audit objectives + let audit_targets: Vec> = vec![Box::new(FileAuditTarget)]; + let audit_logger = AuditLogger::new(audit_targets); + + // Test the PUT operation + put_object(&audit_logger, "my-bucket".to_string(), "my-object.txt".to_string(), "user123".to_string()).await; + + // Wait for the export to complete + tokio::time::sleep(Duration::from_secs(2)).await; + drop(telemetry); // Make sure to clean up + } +} diff --git a/packages/logging/src/logger.rs b/packages/logging/src/logger.rs new file mode 100644 index 000000000..67c8e630c --- /dev/null +++ b/packages/logging/src/logger.rs @@ -0,0 +1,46 @@ +use tracing::{debug, error, info}; + +/// Log an info message +/// +/// # Arguments +/// msg: &str - The message to log +/// +/// # Example +/// ``` +/// use rustfs_logging::log_info; +/// +/// log_info("This is an info message"); +/// ``` +pub fn log_info(msg: &str) { + info!("{}", msg); +} + +/// Log an error message +/// +/// # Arguments +/// msg: &str - The message to log +/// +/// # Example +/// ``` +/// use rustfs_logging::log_error; +/// +/// log_error("This is an error message"); +/// ``` +pub fn log_error(msg: &str) { + error!("{}", msg); +} + +/// Log a debug message +/// +/// # Arguments +/// msg: &str - The message to log +/// +/// # Example +/// ``` +/// use rustfs_logging::log_debug; +/// +/// log_debug("This is a debug message"); +/// ``` +pub fn log_debug(msg: &str) { + debug!("{}", msg); +} diff --git a/packages/logging/src/telemetry.rs b/packages/logging/src/telemetry.rs new file mode 100644 index 000000000..4d1759c49 --- /dev/null +++ b/packages/logging/src/telemetry.rs @@ -0,0 +1,113 @@ +use opentelemetry::trace::TracerProvider; +use opentelemetry::{global, KeyValue}; +use opentelemetry_otlp::{self, WithExportConfig}; +use opentelemetry_sdk::{ + metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}, + trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, + Resource, +}; +use opentelemetry_semantic_conventions::{ + attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION}, + SCHEMA_URL, +}; +use tracing::Level; +use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +/// Telemetry is a wrapper around the OpenTelemetry SDK Tracer and Meter providers. +/// It initializes the global Tracer and Meter providers, and sets up the tracing subscriber. +/// The Tracer and Meter providers are shut down when the Telemetry instance is dropped. +/// This is a convenience struct to ensure that the global providers are properly initialized and shut down. +/// +/// # Example +/// ``` +/// use rustfs_logging::Telemetry; +/// +/// let _telemetry = Telemetry::init(); +/// ``` +pub struct Telemetry { + tracer_provider: SdkTracerProvider, + meter_provider: SdkMeterProvider, +} + +impl Telemetry { + pub fn init() -> Self { + // Define service resource information + let resource = Resource::builder() + .with_schema_url( + [ + KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")), + KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")), + KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, "develop"), + ], + SCHEMA_URL, + ) + .build(); + + let tracer_exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint("http://localhost:4317") + .build() + .unwrap(); + + // Configure Tracer Provider + let tracer_provider = SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_id_generator(RandomIdGenerator::default()) + .with_resource(resource.clone()) + .with_batch_exporter(tracer_exporter) + .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + .build(); + + let meter_exporter = opentelemetry_otlp::MetricExporter::builder() + .with_tonic() + .with_endpoint("http://localhost:4317") + .with_temporality(opentelemetry_sdk::metrics::Temporality::default()) + .build() + .unwrap(); + + let meter_reader = PeriodicReader::builder(meter_exporter) + .with_interval(std::time::Duration::from_secs(30)) + .build(); + + // For debugging in development + let meter_stdout_reader = PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build(); + + // Configure Meter Provider + let meter_provider = MeterProviderBuilder::default() + .with_resource(resource) + .with_reader(meter_reader) + .with_reader(meter_stdout_reader) + .build(); + + // Set global Tracer and Meter providers + global::set_tracer_provider(tracer_provider.clone()); + global::set_meter_provider(meter_provider.clone()); + + let tracer = tracer_provider.tracer("tracing-otel-subscriber-rustfs-service"); + + // Configure `tracing subscriber` + tracing_subscriber::registry() + .with(tracing_subscriber::filter::LevelFilter::from_level(Level::DEBUG)) + .with(tracing_subscriber::fmt::layer().with_ansi(true)) + .with(MetricsLayer::new(meter_provider.clone())) + .with(OpenTelemetryLayer::new(tracer)) + .init(); + + Self { + tracer_provider, + meter_provider, + } + } +} + +impl Drop for Telemetry { + fn drop(&mut self) { + if let Err(err) = self.tracer_provider.shutdown() { + eprintln!("{err:?}"); + } + if let Err(err) = self.meter_provider.shutdown() { + eprintln!("{err:?}"); + } + } +} diff --git a/rustfs/src/logging/mod.rs b/rustfs/src/logging/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index a2ed47909..cfd84975a 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -3,8 +3,10 @@ mod auth; mod config; mod console; mod grpc; +mod logging; mod service; mod storage; + use crate::auth::IAMAuth; use clap::Parser; use common::{ @@ -68,7 +70,7 @@ fn main() -> Result<()> { //解析获得到的参数 let opt = config::Opt::parse(); - //设置trace + //设置 trace setup_tracing(); //运行参数 @@ -91,17 +93,17 @@ async fn run(opt: config::Opt) -> Result<()> { debug!("server_address {}", &server_address); - //设置AK和SK + //设置 AK 和 SK iam::init_global_action_cred(Some(opt.access_key.clone()), Some(opt.secret_key.clone())).unwrap(); set_global_rustfs_port(server_port); - //监听地址,端口从参数中获取 + //监听地址,端口从参数中获取 let listener = TcpListener::bind(server_address.clone()).await?; //获取监听地址 let local_addr: SocketAddr = listener.local_addr()?; - // 用于rpc + // 用于 rpc let (endpoint_pools, setup_type) = EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone()) .map_err(|err| Error::from_string(err.to_string()))?; @@ -123,13 +125,13 @@ async fn run(opt: config::Opt) -> Result<()> { .map_err(|err| Error::from_string(err.to_string()))?; // Setup S3 service - // 本项目使用s3s库来实现s3服务 + // 本项目使用 s3s 库来实现 s3 服务 let service = { let store = storage::ecfs::FS::new(); // let mut b = S3ServiceBuilder::new(storage::ecfs::FS::new(server_address.clone(), endpoint_pools).await?); let mut b = S3ServiceBuilder::new(store.clone()); - //显示info信息 + //显示 info 信息 info!("authentication is enabled {}, {}", &opt.access_key, &opt.secret_key); b.set_auth(IAMAuth::new(opt.access_key, opt.secret_key)); From c04536b13249bfebdaee0992c51966e2edaa7906 Mon Sep 17 00:00:00 2001 From: houseme Date: Fri, 21 Feb 2025 18:49:25 +0800 Subject: [PATCH 02/27] feat(logging): add Kafka and Webhook audit targets - Implemented `KafkaAuditTarget` to send audit entries to a Kafka topic. - Implemented `WebhookAuditTarget` to send audit entries to a specified webhook URL. - Updated `AuditLogger` to support multiple audit targets including Kafka and Webhook. - Added examples and documentation for the new audit targets. --- Cargo.lock | 33 ++++++++++++++ Cargo.toml | 1 + packages/logging/Cargo.toml | 8 ++++ packages/logging/src/audit.rs | 84 +++++++++++++++++++++++++++++++++++ packages/logging/src/lib.rs | 12 ++++- 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d6a65c06e..1df7969a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5192,6 +5192,36 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rdkafka" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b52c81ac3cac39c9639b95c20452076e74b8d9a71bc6fc4d83407af2ea6fff" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio", +] + +[[package]] +name = "rdkafka-sys" +version = "4.8.0+2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced38182dc436b3d9df0c77976f37a67134df26b050df1f0006688e46fc4c8be" +dependencies = [ + "libc", + "libz-sys", + "num_enum", + "pkg-config", +] + [[package]] name = "reader" version = "0.0.1" @@ -5557,7 +5587,10 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry-stdout", "opentelemetry_sdk", + "rdkafka", + "reqwest", "serde", + "serde_json", "tokio", "tracing", "tracing-opentelemetry", diff --git a/Cargo.toml b/Cargo.toml index 511155ef1..581799808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ protobuf = "3.7" protos = { path = "./common/protos" } rand = "0.8.5" reqwest = { version = "0.12.12", default-features = false, features = ["rustls-tls", "charset", "http2", "macos-system-configuration", "stream"] } +rdkafka = { version = "0.37", features = ["tokio"] } rfd = "0.15.2" rmp = "0.8.14" rmp-serde = "1.3.0" diff --git a/packages/logging/Cargo.toml b/packages/logging/Cargo.toml index af37740d9..5b8f84493 100644 --- a/packages/logging/Cargo.toml +++ b/packages/logging/Cargo.toml @@ -9,6 +9,11 @@ version.workspace = true [lints] workspace = true +[features] +default = [] +audit-kafka = ["dep:rdkafka", "dep:serde_json"] +audit-webhook = ["dep:reqwest"] + [dependencies] opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } @@ -20,6 +25,9 @@ tracing = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } tokio = { workspace = true, features = ["full"] } +rdkafka = { workspace = true, features = ["tokio"], optional = true } +reqwest = { workspace = true, features = ["json"], optional = true } +serde_json = { workspace = true, optional = true } diff --git a/packages/logging/src/audit.rs b/packages/logging/src/audit.rs index 0f120db2c..311d31044 100644 --- a/packages/logging/src/audit.rs +++ b/packages/logging/src/audit.rs @@ -1,3 +1,12 @@ +#[cfg(feature = "audit-kafka")] +use rdkafka::{ + producer::{FutureProducer, FutureRecord}, + ClientConfig, +}; + +#[cfg(feature = "audit-webhook")] +use reqwest::Client; + use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; @@ -72,6 +81,80 @@ impl AuditTarget for FileAuditTarget { } } +#[cfg(feature = "audit-webhook")] +/// Webhook audit objectives +pub struct WebhookAuditTarget { + client: Client, + url: String, +} + +#[cfg(feature = "audit-webhook")] +impl WebhookAuditTarget { + pub fn new(url: &str) -> Self { + Self { + client: Client::new(), + url: url.to_string(), + } + } +} + +#[cfg(feature = "audit-webhook")] +impl AuditTarget for WebhookAuditTarget { + fn send(&self, entry: AuditEntry) { + let client = self.client.clone(); + let url = self.url.clone(); + tokio::spawn(async move { + if let Err(e) = client.post(&url).json(&entry).send().await { + eprintln!("Failed to send to Webhook: {:?}", e); + } + }); + } +} + +#[cfg(feature = "audit-kafka")] +/// Kafka audit objectives +pub struct KafkaAuditTarget { + producer: FutureProducer, + topic: String, +} + +#[cfg(feature = "audit-kafka")] +impl KafkaAuditTarget { + pub fn new(brokers: &str, topic: &str) -> Self { + let producer: FutureProducer = ClientConfig::new() + .set("bootstrap.servers", brokers) + .set("message.timeout.ms", "5000") + .create() + .expect("Kafka producer creation failed"); + Self { + producer, + topic: topic.to_string(), + } + } +} + +#[cfg(feature = "audit-kafka")] +impl AuditTarget for KafkaAuditTarget { + fn send(&self, entry: AuditEntry) { + let topic = self.topic.clone(); + let span_id = entry.span_id.clone(); + let payload = serde_json::to_string(&entry).unwrap(); + // let record = FutureRecord::to(&topic).payload(&payload).key(&span_id); + tokio::spawn({ + // 在异步闭包内部创建 record + let topic = topic; + let payload = payload; + let span_id = span_id; + let producer = self.producer.clone(); + async move { + let record = FutureRecord::to(&topic).payload(&payload).key(&span_id); + if let Err(e) = producer.send(record, std::time::Duration::from_secs(0)).await { + eprintln!("Failed to send to Kafka: {:?}", e); + } + } + }); + } +} /// AuditLogger is a logger that logs audit entries /// to multiple targets /// @@ -110,6 +193,7 @@ impl AuditLogger { /// # Example /// ``` /// use rustfs_logging::{AuditLogger, AuditEntry, FileAuditTarget}; + /// /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); /// ``` pub fn new(targets: Vec>) -> Self { diff --git a/packages/logging/src/lib.rs b/packages/logging/src/lib.rs index 3fdbb83d8..280d9e76a 100644 --- a/packages/logging/src/lib.rs +++ b/packages/logging/src/lib.rs @@ -9,6 +9,10 @@ /// log_info("This is an informational message"); /// log_error("This is an error message"); /// ``` +#[cfg(feature = "audit-kafka")] +pub use audit::KafkaAuditTarget; +#[cfg(feature = "audit-webhook")] +pub use audit::WebhookAuditTarget; pub use audit::{AuditEntry, AuditLogger, AuditTarget, FileAuditTarget}; pub use logger::{log_debug, log_error, log_info}; pub use telemetry::Telemetry; @@ -66,11 +70,17 @@ mod tests { } #[tokio::test] + // #[cfg(feature = "audit-webhook")] + // #[cfg(feature = "audit-kafka")] async fn test_main() { let telemetry = Telemetry::init(); // Initialize multiple audit objectives - let audit_targets: Vec> = vec![Box::new(FileAuditTarget)]; + let audit_targets: Vec> = vec![ + Box::new(FileAuditTarget), + // Box::new(KafkaAuditTarget::new("localhost:9092", "rustfs-audit")), + // Box::new(WebhookAuditTarget::new("http://localhost:8080/audit")), + ]; let audit_logger = AuditLogger::new(audit_targets); // Test the PUT operation From b117281c58ed84f4acfee9a2e28b7419f0f237c8 Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 23 Feb 2025 16:38:41 +0800 Subject: [PATCH 03/27] improve code for audit --- Cargo.lock | 18 ++++++++ packages/logging/src/audit.rs | 80 ++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 65f3a58fe..d4a69555b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3523,6 +3523,18 @@ dependencies = [ "x11", ] +[[package]] +name = "libz-sys" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -7085,6 +7097,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/packages/logging/src/audit.rs b/packages/logging/src/audit.rs index 311d31044..712ed088a 100644 --- a/packages/logging/src/audit.rs +++ b/packages/logging/src/audit.rs @@ -60,6 +60,7 @@ impl AuditTarget for FileAuditTarget { /// Send an audit entry to a file /// # Arguments /// * `entry` - The audit entry to send + /// /// # Example /// ``` /// use rustfs_logging::{AuditEntry, AuditTarget, FileAuditTarget}; @@ -75,7 +76,6 @@ impl AuditTarget for FileAuditTarget { /// }; /// FileAuditTarget.send(entry); /// ``` - /// fn send(&self, entry: AuditEntry) { println!("File audit: {:?}", entry); } @@ -83,6 +83,14 @@ impl AuditTarget for FileAuditTarget { #[cfg(feature = "audit-webhook")] /// Webhook audit objectives +/// #Arguments +/// * `client` - The reqwest client +/// * `url` - The URL of the webhook +/// # Example +/// ``` +/// use rustfs_logging::WebhookAuditTarget; +/// let target = WebhookAuditTarget::new("http://localhost:8080"); +/// ``` pub struct WebhookAuditTarget { client: Client, url: String, @@ -113,6 +121,52 @@ impl AuditTarget for WebhookAuditTarget { #[cfg(feature = "audit-kafka")] /// Kafka audit objectives +/// # Arguments +/// * `producer` - The Kafka producer +/// * `topic` - The Kafka topic +/// # Example +/// ``` +/// use rustfs_logging::KafkaAuditTarget; +/// let target = KafkaAuditTarget::new("localhost:9092", "rustfs-audit"); +/// ``` +/// # Note +/// This feature requires the `rdkafka` crate +/// # Example +/// ```toml +/// [dependencies] +/// rdkafka = "0.26.0" +/// rustfs_logging = { version = "0.1.0", features = ["audit-kafka"] } +/// ``` +/// # Note +/// The `rdkafka` crate requires the `librdkafka` library to be installed +/// # Example +/// ```sh +/// sudo apt-get install librdkafka-dev +/// ``` +/// # Note +/// The `rdkafka` crate requires the `libssl-dev` and `pkg-config` packages to be installed +/// # Example +/// ```sh +/// sudo apt-get install libssl-dev pkg-config +/// ``` +/// # Note +/// The `rdkafka` crate requires the `zlib1g-dev` package to be installed +/// # Example +/// ```sh +/// sudo apt-get install zlib1g-dev +/// ``` +/// # Note +/// The `rdkafka` crate requires the `zstd` package to be installed +/// # Example +/// ```sh +/// sudo apt-get install zstd +/// ``` +/// # Note +/// The `rdkafka` crate requires the `lz4` package to be installed +/// # Example +/// ```sh +/// sudo apt-get install lz4 +/// ``` pub struct KafkaAuditTarget { producer: FutureProducer, topic: String, @@ -178,7 +232,31 @@ impl AuditTarget for KafkaAuditTarget { /// logger.log(entry).await; /// } /// ``` + #[derive(Debug)] +/// AuditLogger is a logger that logs audit entries +/// to multiple targets +/// # Example +/// ``` +/// use rustfs_logging::{AuditEntry, AuditLogger, FileAuditTarget}; +/// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); +/// ``` +/// # Note +/// This feature requires the `tokio` crate +/// # Example +/// ```toml +/// [dependencies] +/// tokio = { version = "1", features = ["full"] } +/// rustfs_logging = { version = "0.1.0"} +/// ``` +/// # Note +/// This feature requires the `serde` crate +/// # Example +/// ```toml +/// [dependencies] +/// serde = { version = "1", features = ["derive"] } +/// rustfs_logging = { version = "0.1.0"} +/// ``` pub struct AuditLogger { tx: mpsc::Sender, } From 5ca101b81c4af6a71f20753e8fa548982610b13c Mon Sep 17 00:00:00 2001 From: houseme Date: Thu, 27 Feb 2025 19:00:47 +0800 Subject: [PATCH 04/27] add --- Cargo.lock | 15 +++++ Cargo.toml | 19 ++++++ packages/logging/Cargo.toml | 1 + packages/logging/src/audit.rs | 6 +- packages/logging/src/lib.rs | 101 +++++++++++++++++++++++++++++- packages/logging/src/telemetry.rs | 58 +++++++++++++---- 6 files changed, 186 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4a69555b..5f75b06b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4278,6 +4278,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c513c7af3bec30113f3d4620134ff923295f1e9c580fda2b8abe0831f925ddc0" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-log", + "tracing-opentelemetry", + "tracing-subscriber", +] + [[package]] name = "opentelemetry-http" version = "0.28.0" @@ -5546,6 +5560,7 @@ version = "0.0.1" dependencies = [ "chrono", "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry-stdout", diff --git a/Cargo.toml b/Cargo.toml index 8cad324fa..f9a80d9d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ lazy_static = "1.5.0" mime = "0.3.17" netif = "0.1.6" opentelemetry = { version = "0.28" } +opentelemetry-appender-tracing = { version = "0.28.1" } opentelemetry_sdk = { version = "0.28" } opentelemetry-stdout = { version = "0.28.0" } opentelemetry-otlp = { version = "0.28" } @@ -119,3 +120,21 @@ md-5 = "0.10.6" workers = { path = "./common/workers" } test-case = "3.3.1" zip = "2.2.2" + + +[profile] + +[profile.wasm-dev] +inherits = "dev" +opt-level = 1 + +[profile.server-dev] +inherits = "dev" + +[profile.android-dev] +inherits = "dev" + +[profile.release] +opt-level = 3 # Optimization Level (0-3) +lto = true # Optimize when linking +codegen-units = 1 # Reduce code generation units to improve optimization \ No newline at end of file diff --git a/packages/logging/Cargo.toml b/packages/logging/Cargo.toml index 5b8f84493..73c7c3f97 100644 --- a/packages/logging/Cargo.toml +++ b/packages/logging/Cargo.toml @@ -16,6 +16,7 @@ audit-webhook = ["dep:reqwest"] [dependencies] opentelemetry = { workspace = true } +opentelemetry-appender-tracing = { workspace = true, features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } opentelemetry-stdout = { workspace = true } opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "metrics"] } diff --git a/packages/logging/src/audit.rs b/packages/logging/src/audit.rs index 712ed088a..a8c5d5a24 100644 --- a/packages/logging/src/audit.rs +++ b/packages/logging/src/audit.rs @@ -86,7 +86,7 @@ impl AuditTarget for FileAuditTarget { /// #Arguments /// * `client` - The reqwest client /// * `url` - The URL of the webhook -/// # Example +/// # Example /// ``` /// use rustfs_logging::WebhookAuditTarget; /// let target = WebhookAuditTarget::new("http://localhost:8080"); @@ -310,6 +310,10 @@ impl AuditLogger { /// } /// ``` pub async fn log(&self, entry: AuditEntry) { + // 将日志消息记录到当前 Span + tracing::Span::current() + .record("log_message", &entry.bucket) + .record("source", &entry.event_type); let _ = self.tx.send(entry).await; } } diff --git a/packages/logging/src/lib.rs b/packages/logging/src/lib.rs index 280d9e76a..70e1eb2a5 100644 --- a/packages/logging/src/lib.rs +++ b/packages/logging/src/lib.rs @@ -1,4 +1,5 @@ //! Logging utilities + /// /// This crate provides utilities for logging. /// @@ -24,8 +25,9 @@ mod telemetry; #[cfg(test)] mod tests { use super::*; + use chrono::Utc; use opentelemetry::global; - use opentelemetry::trace::TraceContextExt; + use opentelemetry::trace::{TraceContextExt, Tracer}; use std::time::{Duration, SystemTime}; use tracing::{instrument, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -60,7 +62,7 @@ mod tests { bucket, object, user, - time: chrono::Utc::now().to_rfc3339(), + time: Utc::now().to_rfc3339(), user_agent: "rustfs.rs-client".to_string(), span_id, }; @@ -83,11 +85,106 @@ mod tests { ]; let audit_logger = AuditLogger::new(audit_targets); + // 创建根 Span 并执行操作 + // let tracer = global::tracer("main"); + // tracer.in_span("main_operation", |cx| { + // Span::current().set_parent(cx); + // log_info("Starting test async"); + // tokio::runtime::Runtime::new().unwrap().block_on(async { + log_info("Starting test"); // Test the PUT operation put_object(&audit_logger, "my-bucket".to_string(), "my-object.txt".to_string(), "user123".to_string()).await; + tokio::time::sleep(Duration::from_millis(100)).await; + query_object(&audit_logger, "my-bucket".to_string(), "my-object.txt".to_string(), "user123".to_string()).await; + tokio::time::sleep(Duration::from_millis(100)).await; + for i in 0..100 { + put_object( + &audit_logger, + format!("my-bucket-{}", i), + format!("my-object-{}", i), + "user123".to_string(), + ) + .await; + tokio::time::sleep(Duration::from_millis(100)).await; + query_object( + &audit_logger, + format!("my-bucket-{}", i), + format!("my-object-{}", i), + "user123".to_string(), + ) + .await; + tokio::time::sleep(Duration::from_millis(100)).await; + } // Wait for the export to complete tokio::time::sleep(Duration::from_secs(2)).await; + log_info("Test completed"); + // }); + // }); drop(telemetry); // Make sure to clean up } + + #[instrument(fields(bucket, object, user))] + async fn query_object(audit_logger: &AuditLogger, bucket: String, object: String, user: String) { + let start_time = SystemTime::now(); + log_info("Starting query operation"); + + // Simulate the operation + tokio::time::sleep(Duration::from_millis(100)).await; + + // Record Metrics + let meter = global::meter("rustfs.rs"); + let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); + request_duration.record( + start_time.elapsed().unwrap().as_secs_f64(), + &[opentelemetry::KeyValue::new("operation", "query_object")], + ); + + // Gets the current span + let span = Span::current(); + // Use 'OpenTelemetrySpanExt' to get 'SpanContext' + let span_context = span.context(); // Get context via OpenTelemetrySpanExt + let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId + query_one(user.clone()); + query_two(user.clone()); + query_three(user.clone()); + // Audit events are logged + let audit_entry = AuditEntry { + version: "1.0".to_string(), + event_type: "s3_query_object".to_string(), + bucket, + object, + user, + time: Utc::now().to_rfc3339(), + user_agent: "rustfs.rs-client".to_string(), + span_id, + }; + audit_logger.log(audit_entry).await; + + log_info("query operation completed"); + } + #[instrument(fields(user))] + fn query_one(user: String) { + // 初始化 OpenTelemetry Tracer + let tracer = global::tracer("query_one"); + tracer.in_span("doing_work", |cx| { + // Traced app logic here... + Span::current().set_parent(cx); + log_info("Doing work..."); + let current_span = Span::current(); + let span_context = current_span.context(); + let trace_id = span_context.clone().span().span_context().trace_id().to_string(); + let span_id = span_context.clone().span().span_context().span_id().to_string(); + log_info(format!("trace_id: {}, span_id: {}", trace_id, span_id).as_str()); + }); + log_info(format!("Starting query_one operation user:{}", user).as_str()); + } + #[instrument(fields(user))] + fn query_two(user: String) { + log_info(format!("Starting query_two operation user:{}", user).as_str()); + } + #[instrument(fields(user))] + fn query_three(user: String) { + log_info(format!("Starting query_three operation user: {}", user).as_str()); + } } diff --git a/packages/logging/src/telemetry.rs b/packages/logging/src/telemetry.rs index 4d1759c49..edd362cd9 100644 --- a/packages/logging/src/telemetry.rs +++ b/packages/logging/src/telemetry.rs @@ -1,18 +1,22 @@ use opentelemetry::trace::TracerProvider; use opentelemetry::{global, KeyValue}; +use opentelemetry_appender_tracing::layer; use opentelemetry_otlp::{self, WithExportConfig}; +use opentelemetry_sdk::logs::SdkLoggerProvider; use opentelemetry_sdk::{ metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}, trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, Resource, }; +use opentelemetry_semantic_conventions::attribute::NETWORK_LOCAL_ADDRESS; use opentelemetry_semantic_conventions::{ attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION}, SCHEMA_URL, }; -use tracing::Level; +use std::time::Duration; +use tracing::{info, Level}; use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; /// Telemetry is a wrapper around the OpenTelemetry SDK Tracer and Meter providers. /// It initializes the global Tracer and Meter providers, and sets up the tracing subscriber. @@ -34,11 +38,13 @@ impl Telemetry { pub fn init() -> Self { // Define service resource information let resource = Resource::builder() + .with_service_name("rustfs-service") .with_schema_url( [ - KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")), - KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")), + KeyValue::new(SERVICE_NAME, "rustfs-service"), + KeyValue::new(SERVICE_VERSION, "0.1.0"), KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, "develop"), + KeyValue::new(NETWORK_LOCAL_ADDRESS, "127.0.0.1"), ], SCHEMA_URL, ) @@ -47,6 +53,8 @@ impl Telemetry { let tracer_exporter = opentelemetry_otlp::SpanExporter::builder() .with_tonic() .with_endpoint("http://localhost:4317") + .with_protocol(opentelemetry_otlp::Protocol::Grpc) + .with_timeout(Duration::from_secs(3)) .build() .unwrap(); @@ -56,35 +64,61 @@ impl Telemetry { .with_id_generator(RandomIdGenerator::default()) .with_resource(resource.clone()) .with_batch_exporter(tracer_exporter) - .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + // .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) .build(); let meter_exporter = opentelemetry_otlp::MetricExporter::builder() .with_tonic() .with_endpoint("http://localhost:4317") + .with_protocol(opentelemetry_otlp::Protocol::Grpc) + .with_timeout(Duration::from_secs(3)) .with_temporality(opentelemetry_sdk::metrics::Temporality::default()) .build() .unwrap(); let meter_reader = PeriodicReader::builder(meter_exporter) - .with_interval(std::time::Duration::from_secs(30)) + .with_interval(Duration::from_secs(30)) .build(); // For debugging in development - let meter_stdout_reader = PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build(); + // let meter_stdout_reader = PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build(); // Configure Meter Provider let meter_provider = MeterProviderBuilder::default() - .with_resource(resource) + .with_resource(resource.clone()) .with_reader(meter_reader) - .with_reader(meter_stdout_reader) + // .with_reader(meter_stdout_reader) .build(); // Set global Tracer and Meter providers global::set_tracer_provider(tracer_provider.clone()); global::set_meter_provider(meter_provider.clone()); - let tracer = tracer_provider.tracer("tracing-otel-subscriber-rustfs-service"); + let tracer = tracer_provider.tracer("rustfs-service"); + + // // let _stdout_exporter = opentelemetry_stdout::LogExporter::default(); + // let otlp_exporter = opentelemetry_otlp::LogExporter::builder() + // .with_tonic() + // .with_endpoint("http://localhost:4317") + // // .with_timeout(Duration::from_secs(3)) + // // .with_protocol(opentelemetry_otlp::Protocol::Grpc) + // .build() + // .unwrap(); + // let provider: SdkLoggerProvider = SdkLoggerProvider::builder() + // .with_resource(resource.clone()) + // .with_simple_exporter(otlp_exporter) + // .build(); + // let filter_otel = EnvFilter::new("debug") + // // .add_directive("hyper=off".parse().unwrap()) + // // .add_directive("opentelemetry=off".parse().unwrap()) + // // .add_directive("tonic=off".parse().unwrap()) + // // .add_directive("h2=off".parse().unwrap()) + // .add_directive("reqwest=off".parse().unwrap()); + // let otel_layer = layer::OpenTelemetryTracingBridge::new(&provider).with_filter(filter_otel); + let filter_fmt = EnvFilter::new("info").add_directive("opentelemetry=debug".parse().unwrap()); + let fmt_layer = tracing_subscriber::fmt::layer() + .with_thread_names(true) + .with_filter(filter_fmt); // Configure `tracing subscriber` tracing_subscriber::registry() @@ -92,8 +126,10 @@ impl Telemetry { .with(tracing_subscriber::fmt::layer().with_ansi(true)) .with(MetricsLayer::new(meter_provider.clone())) .with(OpenTelemetryLayer::new(tracer)) + // .with(otel_layer) + .with(fmt_layer) .init(); - + info!(name: "my-event-name", target: "my-system", event_id = 20, user_name = "otel", user_email = "otel@opentelemetry.io", message = "This is an example message"); Self { tracer_provider, meter_provider, From bcab86fc388adce01b1f7cd84fa728ee66c72e6c Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 9 Mar 2025 10:47:03 +0800 Subject: [PATCH 05/27] rename `rustfs-logging` to `rustfs-obs` --- Cargo.lock | 172 +++++++++++++++++++-- Cargo.toml | 4 +- packages/{logging => obs}/Cargo.toml | 2 +- packages/{logging => obs}/src/audit.rs | 22 +-- packages/{logging => obs}/src/lib.rs | 4 +- packages/{logging => obs}/src/logger.rs | 6 +- packages/{logging => obs}/src/telemetry.rs | 2 +- rustfs/Cargo.toml | 1 + 8 files changed, 181 insertions(+), 32 deletions(-) rename packages/{logging => obs}/Cargo.toml (98%) rename packages/{logging => obs}/src/audit.rs (93%) rename packages/{logging => obs}/src/lib.rs (98%) rename packages/{logging => obs}/src/logger.rs (86%) rename packages/{logging => obs}/src/telemetry.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index a99f98d83..fb04b7c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1377,7 +1377,7 @@ dependencies = [ "futures-util", "generational-box", "longest-increasing-subsequence", - "rustc-hash", + "rustc-hash 1.1.0", "rustversion", "serde", "slab", @@ -1443,7 +1443,7 @@ dependencies = [ "objc_id", "once_cell", "rfd 0.14.1", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde_json", "signal-hook", @@ -1604,7 +1604,7 @@ dependencies = [ "dioxus-html", "js-sys", "lazy-js-bundle", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "sledgehammer_bindgen", "sledgehammer_utils", @@ -1710,7 +1710,7 @@ dependencies = [ "generational-box", "once_cell", "parking_lot 0.12.3", - "rustc-hash", + "rustc-hash 1.1.0", "tracing", "warnings", ] @@ -1737,7 +1737,7 @@ dependencies = [ "generational-box", "js-sys", "lazy-js-bundle", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde-wasm-bindgen", "serde_json", @@ -1971,6 +1971,15 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -2881,6 +2890,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -4912,6 +4939,58 @@ dependencies = [ "serde", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.11", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.15", + "rand 0.8.5", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.11", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.38" @@ -5197,19 +5276,16 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64", "bytes", -<<<<<<< HEAD "encoding_rs", "futures-channel", -||||||| e65bee0 - "encoding_rs", -======= ->>>>>>> 1d58a07f296b9dedba832bb31b31eb9faf38b85c "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "ipnet", "js-sys", @@ -5219,11 +5295,17 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", + "tokio-rustls", "tokio-util", "tower 0.5.2", "tower-service", @@ -5232,6 +5314,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "windows-registry", ] @@ -5365,6 +5448,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -5416,6 +5505,7 @@ dependencies = [ "protos", "rmp-serde", "rust-embed", + "rustfs-obs", "s3s", "serde", "serde_json", @@ -5460,7 +5550,7 @@ dependencies = [ ] [[package]] -name = "rustfs-logging" +name = "rustfs-obs" version = "0.0.1" dependencies = [ "chrono", @@ -5522,6 +5612,9 @@ name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -6024,7 +6117,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" dependencies = [ - "rustc-hash", + "rustc-hash 1.1.0", ] [[package]] @@ -6208,6 +6301,27 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -6435,6 +6549,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.43.0" @@ -7227,6 +7356,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webbrowser" version = "0.8.15" @@ -7288,6 +7427,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.33.0" diff --git a/Cargo.toml b/Cargo.toml index 6915a975a..70e5e3888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ "iam", # Identity and Access Management "crypto", # Cryptography and security features "cli/rustfs-gui", # Graphical user interface client - "packages/logging", # Logging utilities + "packages/obs", # Observability utilities ] resolver = "2" @@ -81,7 +81,7 @@ rdkafka = { version = "0.37", features = ["tokio"] } rfd = { version = "0.15.2", default-features = false, features = ["xdg-portal", "tokio"] } rmp = "0.8.14" rmp-serde = "1.3.0" -rustfs-logging = { path = "packages/logging", version = "0.0.1" } +rustfs-obs = { path = "packages/obs", version = "0.0.1" } rust-embed = "8.6.0" s3s = { git = "https://github.com/Nugine/s3s.git", rev = "ab139f72fe768fb9d8cecfe36269451da1ca9779", default-features = true, features = [ "tower", diff --git a/packages/logging/Cargo.toml b/packages/obs/Cargo.toml similarity index 98% rename from packages/logging/Cargo.toml rename to packages/obs/Cargo.toml index 73c7c3f97..0e5b3407f 100644 --- a/packages/logging/Cargo.toml +++ b/packages/obs/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rustfs-logging" +name = "rustfs-obs" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/packages/logging/src/audit.rs b/packages/obs/src/audit.rs similarity index 93% rename from packages/logging/src/audit.rs rename to packages/obs/src/audit.rs index a8c5d5a24..b353d83fb 100644 --- a/packages/logging/src/audit.rs +++ b/packages/obs/src/audit.rs @@ -23,7 +23,7 @@ use tokio::sync::mpsc; /// * `span_id` - The span ID of the event /// # Example /// ``` -/// use rustfs_logging::AuditEntry; +/// use rustfs_obs::AuditEntry; /// let entry = AuditEntry { /// version: "1.0".to_string(), /// event_type: "read".to_string(), @@ -63,7 +63,7 @@ impl AuditTarget for FileAuditTarget { /// /// # Example /// ``` - /// use rustfs_logging::{AuditEntry, AuditTarget, FileAuditTarget}; + /// use rustfs_obs::{AuditEntry, AuditTarget, FileAuditTarget}; /// let entry = AuditEntry { /// version: "1.0".to_string(), /// event_type: "read".to_string(), @@ -88,7 +88,7 @@ impl AuditTarget for FileAuditTarget { /// * `url` - The URL of the webhook /// # Example /// ``` -/// use rustfs_logging::WebhookAuditTarget; +/// use rustfs_obs::WebhookAuditTarget; /// let target = WebhookAuditTarget::new("http://localhost:8080"); /// ``` pub struct WebhookAuditTarget { @@ -126,7 +126,7 @@ impl AuditTarget for WebhookAuditTarget { /// * `topic` - The Kafka topic /// # Example /// ``` -/// use rustfs_logging::KafkaAuditTarget; +/// use rustfs_obs::KafkaAuditTarget; /// let target = KafkaAuditTarget::new("localhost:9092", "rustfs-audit"); /// ``` /// # Note @@ -135,7 +135,7 @@ impl AuditTarget for WebhookAuditTarget { /// ```toml /// [dependencies] /// rdkafka = "0.26.0" -/// rustfs_logging = { version = "0.1.0", features = ["audit-kafka"] } +/// rustfs_obs = { version = "0.1.0", features = ["audit-kafka"] } /// ``` /// # Note /// The `rdkafka` crate requires the `librdkafka` library to be installed @@ -214,7 +214,7 @@ impl AuditTarget for KafkaAuditTarget { /// /// # Example /// ``` -/// use rustfs_logging::{AuditEntry, AuditLogger, FileAuditTarget}; +/// use rustfs_obs::{AuditEntry, AuditLogger, FileAuditTarget}; /// /// #[tokio::main] /// async fn main() { @@ -238,7 +238,7 @@ impl AuditTarget for KafkaAuditTarget { /// to multiple targets /// # Example /// ``` -/// use rustfs_logging::{AuditEntry, AuditLogger, FileAuditTarget}; +/// use rustfs_obs::{AuditEntry, AuditLogger, FileAuditTarget}; /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); /// ``` /// # Note @@ -247,7 +247,7 @@ impl AuditTarget for KafkaAuditTarget { /// ```toml /// [dependencies] /// tokio = { version = "1", features = ["full"] } -/// rustfs_logging = { version = "0.1.0"} +/// rustfs_obs = { version = "0.1.0"} /// ``` /// # Note /// This feature requires the `serde` crate @@ -255,7 +255,7 @@ impl AuditTarget for KafkaAuditTarget { /// ```toml /// [dependencies] /// serde = { version = "1", features = ["derive"] } -/// rustfs_logging = { version = "0.1.0"} +/// rustfs_obs = { version = "0.1.0"} /// ``` pub struct AuditLogger { tx: mpsc::Sender, @@ -270,7 +270,7 @@ impl AuditLogger { /// * An AuditLogger /// # Example /// ``` - /// use rustfs_logging::{AuditLogger, AuditEntry, FileAuditTarget}; + /// use rustfs_obs::{AuditLogger, AuditEntry, FileAuditTarget}; /// /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); /// ``` @@ -291,7 +291,7 @@ impl AuditLogger { /// * `entry` - The audit entry to log /// # Example /// ``` - /// use rustfs_logging::{AuditEntry, AuditLogger, FileAuditTarget}; + /// use rustfs_obs::{AuditEntry, AuditLogger, FileAuditTarget}; /// /// #[tokio::main] /// async fn main() { diff --git a/packages/logging/src/lib.rs b/packages/obs/src/lib.rs similarity index 98% rename from packages/logging/src/lib.rs rename to packages/obs/src/lib.rs index 70e1eb2a5..caf993220 100644 --- a/packages/logging/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -5,7 +5,7 @@ /// /// # Examples /// ``` -/// use rustfs_logging::{log_info, log_error}; +/// use rustfs_obs::{log_info, log_error}; /// /// log_info("This is an informational message"); /// log_error("This is an error message"); @@ -24,7 +24,7 @@ mod telemetry; #[cfg(test)] mod tests { - use super::*; + use crate::{log_info, AuditEntry, AuditLogger, AuditTarget, FileAuditTarget, Telemetry}; use chrono::Utc; use opentelemetry::global; use opentelemetry::trace::{TraceContextExt, Tracer}; diff --git a/packages/logging/src/logger.rs b/packages/obs/src/logger.rs similarity index 86% rename from packages/logging/src/logger.rs rename to packages/obs/src/logger.rs index 67c8e630c..023419592 100644 --- a/packages/logging/src/logger.rs +++ b/packages/obs/src/logger.rs @@ -7,7 +7,7 @@ use tracing::{debug, error, info}; /// /// # Example /// ``` -/// use rustfs_logging::log_info; +/// use rustfs_obs::log_info; /// /// log_info("This is an info message"); /// ``` @@ -22,7 +22,7 @@ pub fn log_info(msg: &str) { /// /// # Example /// ``` -/// use rustfs_logging::log_error; +/// use rustfs_obs::log_error; /// /// log_error("This is an error message"); /// ``` @@ -37,7 +37,7 @@ pub fn log_error(msg: &str) { /// /// # Example /// ``` -/// use rustfs_logging::log_debug; +/// use rustfs_obs::log_debug; /// /// log_debug("This is a debug message"); /// ``` diff --git a/packages/logging/src/telemetry.rs b/packages/obs/src/telemetry.rs similarity index 99% rename from packages/logging/src/telemetry.rs rename to packages/obs/src/telemetry.rs index edd362cd9..0eed5466d 100644 --- a/packages/logging/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -25,7 +25,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte /// /// # Example /// ``` -/// use rustfs_logging::Telemetry; +/// use rustfs_obs::Telemetry; /// /// let _telemetry = Telemetry::init(); /// ``` diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index e27c59575..b156e2c96 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -76,6 +76,7 @@ mime_guess = "2.0.5" rust-embed = { workspace = true, features = ["interpolate-folder-path"] } local-ip-address = { workspace = true } chrono = { workspace = true } +rustfs-obs = { workspace = true } [build-dependencies] prost-build.workspace = true From eaa506add5163a351cd6d6066dc7a0ec8670a7e6 Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 12 Mar 2025 00:46:01 +0800 Subject: [PATCH 06/27] improve code for observability --- Cargo.lock | 276 +++++++++++++++--- Cargo.toml | 12 +- packages/obs/Cargo.toml | 18 +- packages/obs/examples/config.toml | 31 ++ packages/obs/examples/server.rs | 78 ++++++ packages/obs/src/audit.rs | 319 --------------------- packages/obs/src/config.rs | 90 ++++++ packages/obs/src/entry.rs | 80 ++++++ packages/obs/src/lib.rs | 210 ++------------ packages/obs/src/logger.rs | 272 +++++++++++++++--- packages/obs/src/sink.rs | 450 ++++++++++++++++++++++++++++++ packages/obs/src/telemetry.rs | 279 +++++++++++------- packages/obs/src/utils.rs | 42 +++ packages/obs/src/worker.rs | 13 + 14 files changed, 1480 insertions(+), 690 deletions(-) create mode 100644 packages/obs/examples/config.toml create mode 100644 packages/obs/examples/server.rs delete mode 100644 packages/obs/src/audit.rs create mode 100644 packages/obs/src/config.rs create mode 100644 packages/obs/src/entry.rs create mode 100644 packages/obs/src/sink.rs create mode 100644 packages/obs/src/utils.rs create mode 100644 packages/obs/src/worker.rs diff --git a/Cargo.lock b/Cargo.lock index fb04b7c42..ca81fe744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,6 +176,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" @@ -352,9 +358,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", @@ -486,6 +492,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -907,6 +919,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.15.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb07d21d12f9f0bc5e7c3e97ccc78b2341b9b4a4604eac3ed7c1d0d6e2c3b23e" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "winnow 0.7.3", + "yaml-rust2", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -923,6 +954,26 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cb3c4a0d3776f7535c32793be81d6d5fec0d48ac70955d9834e643aa249a52f" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + [[package]] name = "const-serialize" version = "0.6.2" @@ -1135,7 +1186,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "test-case", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -1415,7 +1466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b0cca3e7a10a4a3df37ea52c4cc7a53e5c9233489e03ee3f2829471fc3099a" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "cocoa 0.25.0", "core-foundation 0.9.4", "dioxus-cli-config", @@ -1509,7 +1560,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe99b48a1348eec385b5c4bd3e80fd863b0d3b47257d34e2ddc58754dec5d128" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "ciborium", "dioxus-desktop", @@ -1850,6 +1901,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1948,7 +2008,7 @@ dependencies = [ "sha2 0.11.0-pre.4", "siphasher 1.0.1", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -2132,6 +2192,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.5.0" @@ -2720,6 +2786,18 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] [[package]] name = "heck" @@ -2962,7 +3040,7 @@ dependencies = [ "serde_json", "strum", "test-case", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -3287,13 +3365,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonwebtoken" version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -4218,7 +4307,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4265,7 +4354,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tonic", "tracing", @@ -4301,7 +4390,7 @@ dependencies = [ "opentelemetry", "opentelemetry_sdk", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4319,7 +4408,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", "tracing", @@ -4331,6 +4420,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -4473,6 +4572,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -4489,7 +4594,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64", + "base64 0.22.1", "serde", ] @@ -4499,6 +4604,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + [[package]] name = "petgraph" version = "0.7.1" @@ -4952,7 +5102,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -4971,7 +5121,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tracing", "web-time", @@ -4988,7 +5138,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5164,7 +5314,7 @@ dependencies = [ "pin-project-lite", "s3s", "sha2 0.11.0-pre.4", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -5206,7 +5356,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5274,7 +5424,7 @@ version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -5401,6 +5551,18 @@ dependencies = [ "serde", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.0", + "serde", + "serde_derive", +] + [[package]] name = "rust-embed" version = "8.6.0" @@ -5436,6 +5598,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5553,7 +5726,10 @@ dependencies = [ name = "rustfs-obs" version = "0.0.1" dependencies = [ + "async-trait", "chrono", + "config", + "local-ip-address", "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-otlp", @@ -5564,8 +5740,10 @@ dependencies = [ "reqwest", "serde", "serde_json", + "thiserror 2.0.12", "tokio", "tracing", + "tracing-core", "tracing-opentelemetry", "tracing-subscriber", ] @@ -5676,7 +5854,7 @@ dependencies = [ "smallvec", "std-next", "sync_wrapper", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tower 0.5.2", @@ -5694,7 +5872,7 @@ dependencies = [ "indexmap 2.7.1", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6066,7 +6244,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -6197,7 +6375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafdb55260d9b29c04fa52351e0db2a4aaeadc462cd884ccd7771c5a31aaf1aa" dependencies = [ "simdutf8", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6467,11 +6645,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -6487,9 +6665,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -6539,6 +6717,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -6566,9 +6753,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", @@ -6694,7 +6881,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "flate2", "h2", @@ -6960,10 +7147,16 @@ dependencies = [ "objc2-foundation 0.3.0", "once_cell", "png", - "thiserror 2.0.11", + "thiserror 2.0.12", "windows-sys 0.59.0", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -6994,6 +7187,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" @@ -7870,7 +8069,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" dependencies = [ - "base64", + "base64 0.22.1", "block", "cocoa 0.26.0", "core-graphics 0.24.0", @@ -7941,6 +8140,17 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "yaml-rust2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232bdb534d65520716bef0bbb205ff8f2db72d807b19c0bc3020853b92a0cd4b" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 70e5e3888..fff723ded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,12 +32,13 @@ all = "warn" [workspace.dependencies] madmin = { path = "./madmin" } -async-trait = "0.1.86" +async-trait = "0.1.87" backon = "1.3.0" bytes = "1.9.0" bytesize = "1.3.0" chrono = { version = "0.4.40", features = ["serde"] } clap = { version = "4.5.31", features = ["derive", "env"] } +config = "0.15.9" dioxus = { version = "0.6.3", features = ["router"] } dirs = "6.0.0" ecstore = { path = "./ecstore" } @@ -63,7 +64,7 @@ local-ip-address = "0.6.3" mime = "0.3.17" netif = "0.1.6" opentelemetry = { version = "0.28" } -opentelemetry-appender-tracing = { version = "0.28.1" } +opentelemetry-appender-tracing = { version = "0.28.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } opentelemetry_sdk = { version = "0.28" } opentelemetry-stdout = { version = "0.28.0" } opentelemetry-otlp = { version = "0.28" } @@ -76,7 +77,7 @@ prost-types = "0.13.4" protobuf = "3.7" protos = { path = "./common/protos" } rand = "0.8.5" -reqwest = { version = "0.12.12", default-features = false, features = ["rustls-tls", "charset", "http2", "macos-system-configuration", "stream"] } +reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls", "charset", "http2", "macos-system-configuration", "stream"] } rdkafka = { version = "0.37", features = ["tokio"] } rfd = { version = "0.15.2", default-features = false, features = ["xdg-portal", "tokio"] } rmp = "0.8.14" @@ -92,7 +93,7 @@ serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" sha2 = "0.10.8" tempfile = "3.16.0" -thiserror = "2.0.11" +thiserror = "2.0.12" time = { version = "0.3.37", features = [ "std", "parsing", @@ -100,13 +101,14 @@ time = { version = "0.3.37", features = [ "macros", "serde", ] } -tokio = { version = "1.43.0", features = ["fs", "rt-multi-thread"] } +tokio = { version = "1.44.0", features = ["fs", "rt-multi-thread"] } tonic = { version = "0.12.3", features = ["gzip"] } tonic-build = "0.12.3" tonic-reflection = "0.12" tokio-stream = "0.1.17" tower = { version = "0.5.2", features = ["timeout"] } tracing = "0.1.41" +tracing-core = "0.1.33" tracing-error = "0.2.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] } tracing-appender = "0.2.3" diff --git a/packages/obs/Cargo.toml b/packages/obs/Cargo.toml index 0e5b3407f..8753d63c7 100644 --- a/packages/obs/Cargo.toml +++ b/packages/obs/Cargo.toml @@ -10,11 +10,15 @@ version.workspace = true workspace = true [features] -default = [] -audit-kafka = ["dep:rdkafka", "dep:serde_json"] -audit-webhook = ["dep:reqwest"] +default = ["file"] +kafka = ["dep:rdkafka", "dep:serde_json"] +webhook = ["dep:reqwest"] +file = [] [dependencies] +async-trait = { workspace = true } +chrono = { workspace = true } +config = { workspace = true } opentelemetry = { workspace = true } opentelemetry-appender-tracing = { workspace = true, features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } @@ -23,13 +27,15 @@ opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "metrics"] } opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } serde = { workspace = true } tracing = { workspace = true } +tracing-core = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true, features = ["sync", "fs"] } rdkafka = { workspace = true, features = ["tokio"], optional = true } -reqwest = { workspace = true, features = ["json"], optional = true } +reqwest = { workspace = true, optional = true, default-features = false } serde_json = { workspace = true, optional = true } - +thiserror = { workspace = true } +local-ip-address = { workspace = true } [dev-dependencies] diff --git a/packages/obs/examples/config.toml b/packages/obs/examples/config.toml new file mode 100644 index 000000000..9c3c68a26 --- /dev/null +++ b/packages/obs/examples/config.toml @@ -0,0 +1,31 @@ +[observability] +endpoint = "http://localhost:4317" +use_stdout = true +sample_ratio = 0.5 +meter_interval = 30 +service_name = "logging_service" +service_version = "0.1.0" +deployment_environment = "develop" + +[sinks] +[sinks.kafka] +enabled = false +bootstrap_servers = "localhost:9092" +topic = "logs" +batch_size = 100 # Default is 100 if not specified +batch_timeout_ms = 1000 # Default is 1000ms if not specified + +[sinks.webhook] +enabled = false +url = "http://localhost:8080/webhook" +batch_size = 100 # Default is 3 if not specified +batch_timeout_ms = 1000 # Default is 100ms if not specified + +[sinks.file] +enabled = true +path = "app.log" +batch_size = 100 +batch_timeout_ms = 1000 # Default is 8192 bytes if not specified + +[logger] +queue_capacity = 10000 \ No newline at end of file diff --git a/packages/obs/examples/server.rs b/packages/obs/examples/server.rs new file mode 100644 index 000000000..b2c104dd3 --- /dev/null +++ b/packages/obs/examples/server.rs @@ -0,0 +1,78 @@ +use opentelemetry::global; +use opentelemetry::trace::TraceContextExt; +use rustfs_obs::{init_logging, load_config, LogEntry}; +use std::time::{Duration, SystemTime}; +use tracing::{info, instrument, Span}; +use tracing_core::Level; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +#[tokio::main] +async fn main() { + let start_time = SystemTime::now(); + let config = load_config(Some("packages/obs/examples/config".to_string())); + info!("Configuration file loading is complete {:?}", config.clone()); + let (logger, _guard) = init_logging(config); + info!("Log module initialization is completed"); + // Simulate the operation + tokio::time::sleep(Duration::from_millis(100)).await; + + // Record Metrics + let meter = global::meter("rustfs.rs"); + let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); + request_duration.record( + start_time.elapsed().unwrap().as_secs_f64(), + &[opentelemetry::KeyValue::new("operation", "put_object")], + ); + + // Gets the current span + let span = Span::current(); + // Use 'OpenTelemetrySpanExt' to get 'SpanContext' + let span_context = span.context(); // Get context via OpenTelemetrySpanExt + let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId + let trace_id = span_context.span().span_context().trace_id().to_string(); // Get the TraceId + let result = logger + .log(LogEntry::new( + Level::INFO, + "Process user requests".to_string(), + "api_handler".to_string(), + Some("req-12345".to_string()), + Some("user-6789".to_string()), + vec![ + ("endpoint".to_string(), "/api/v1/data".to_string()), + ("method".to_string(), "GET".to_string()), + ("span_id".to_string(), span_id), + ("trace_id".to_string(), trace_id), + ], + )) + .await; + info!("Logging is completed {:?}", result); + put_object("bucket".to_string(), "object".to_string(), "user".to_string()).await; + info!("Logging is completed"); + tokio::time::sleep(Duration::from_secs(2)).await; + info!("Program ends"); +} + +#[instrument(fields(bucket, object, user))] +async fn put_object(bucket: String, object: String, user: String) { + let start_time = SystemTime::now(); + info!("Starting PUT operation"); + // Gets the current span + let span = Span::current(); + // Use 'OpenTelemetrySpanExt' to get 'SpanContext' + let span_context = span.context(); // Get context via OpenTelemetrySpanExt + let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId + let trace_id = span_context.span().span_context().trace_id().to_string(); // Get the TraceId + info!( + "Starting PUT operation content: bucket = {}, object = {}, user = {},span_id = {},trace_id = {},start_time = {}", + bucket, + object, + user, + span_id, + trace_id, + start_time.elapsed().unwrap().as_secs_f64() + ); + // Simulate the operation + tokio::time::sleep(Duration::from_millis(100)).await; + + info!("PUT operation completed"); +} diff --git a/packages/obs/src/audit.rs b/packages/obs/src/audit.rs deleted file mode 100644 index b353d83fb..000000000 --- a/packages/obs/src/audit.rs +++ /dev/null @@ -1,319 +0,0 @@ -#[cfg(feature = "audit-kafka")] -use rdkafka::{ - producer::{FutureProducer, FutureRecord}, - ClientConfig, -}; - -#[cfg(feature = "audit-webhook")] -use reqwest::Client; - -use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; - -/// AuditEntry is a struct that represents an audit entry -/// that can be logged -/// # Fields -/// * `version` - The version of the audit entry -/// * `event_type` - The type of event that occurred -/// * `bucket` - The bucket that was accessed -/// * `object` - The object that was accessed -/// * `user` - The user that accessed the object -/// * `time` - The time the event occurred -/// * `user_agent` - The user agent that accessed the object -/// * `span_id` - The span ID of the event -/// # Example -/// ``` -/// use rustfs_obs::AuditEntry; -/// let entry = AuditEntry { -/// version: "1.0".to_string(), -/// event_type: "read".to_string(), -/// bucket: "bucket".to_string(), -/// object: "object".to_string(), -/// user: "user".to_string(), -/// time: "time".to_string(), -/// user_agent: "user_agent".to_string(), -/// span_id: "span_id".to_string(), -/// }; -/// ``` -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AuditEntry { - pub version: String, - pub event_type: String, - pub bucket: String, - pub object: String, - pub user: String, - pub time: String, - pub user_agent: String, - pub span_id: String, -} - -/// AuditTarget is a trait that defines the interface for audit targets -/// that can receive audit entries -pub trait AuditTarget: Send + Sync { - fn send(&self, entry: AuditEntry); -} - -/// FileAuditTarget is an audit target that logs audit entries to a file -pub struct FileAuditTarget; - -impl AuditTarget for FileAuditTarget { - /// Send an audit entry to a file - /// # Arguments - /// * `entry` - The audit entry to send - /// - /// # Example - /// ``` - /// use rustfs_obs::{AuditEntry, AuditTarget, FileAuditTarget}; - /// let entry = AuditEntry { - /// version: "1.0".to_string(), - /// event_type: "read".to_string(), - /// bucket: "bucket".to_string(), - /// object: "object".to_string(), - /// user: "user".to_string(), - /// time: "time".to_string(), - /// user_agent: "user_agent".to_string(), - /// span_id: "span_id".to_string(), - /// }; - /// FileAuditTarget.send(entry); - /// ``` - fn send(&self, entry: AuditEntry) { - println!("File audit: {:?}", entry); - } -} - -#[cfg(feature = "audit-webhook")] -/// Webhook audit objectives -/// #Arguments -/// * `client` - The reqwest client -/// * `url` - The URL of the webhook -/// # Example -/// ``` -/// use rustfs_obs::WebhookAuditTarget; -/// let target = WebhookAuditTarget::new("http://localhost:8080"); -/// ``` -pub struct WebhookAuditTarget { - client: Client, - url: String, -} - -#[cfg(feature = "audit-webhook")] -impl WebhookAuditTarget { - pub fn new(url: &str) -> Self { - Self { - client: Client::new(), - url: url.to_string(), - } - } -} - -#[cfg(feature = "audit-webhook")] -impl AuditTarget for WebhookAuditTarget { - fn send(&self, entry: AuditEntry) { - let client = self.client.clone(); - let url = self.url.clone(); - tokio::spawn(async move { - if let Err(e) = client.post(&url).json(&entry).send().await { - eprintln!("Failed to send to Webhook: {:?}", e); - } - }); - } -} - -#[cfg(feature = "audit-kafka")] -/// Kafka audit objectives -/// # Arguments -/// * `producer` - The Kafka producer -/// * `topic` - The Kafka topic -/// # Example -/// ``` -/// use rustfs_obs::KafkaAuditTarget; -/// let target = KafkaAuditTarget::new("localhost:9092", "rustfs-audit"); -/// ``` -/// # Note -/// This feature requires the `rdkafka` crate -/// # Example -/// ```toml -/// [dependencies] -/// rdkafka = "0.26.0" -/// rustfs_obs = { version = "0.1.0", features = ["audit-kafka"] } -/// ``` -/// # Note -/// The `rdkafka` crate requires the `librdkafka` library to be installed -/// # Example -/// ```sh -/// sudo apt-get install librdkafka-dev -/// ``` -/// # Note -/// The `rdkafka` crate requires the `libssl-dev` and `pkg-config` packages to be installed -/// # Example -/// ```sh -/// sudo apt-get install libssl-dev pkg-config -/// ``` -/// # Note -/// The `rdkafka` crate requires the `zlib1g-dev` package to be installed -/// # Example -/// ```sh -/// sudo apt-get install zlib1g-dev -/// ``` -/// # Note -/// The `rdkafka` crate requires the `zstd` package to be installed -/// # Example -/// ```sh -/// sudo apt-get install zstd -/// ``` -/// # Note -/// The `rdkafka` crate requires the `lz4` package to be installed -/// # Example -/// ```sh -/// sudo apt-get install lz4 -/// ``` -pub struct KafkaAuditTarget { - producer: FutureProducer, - topic: String, -} - -#[cfg(feature = "audit-kafka")] -impl KafkaAuditTarget { - pub fn new(brokers: &str, topic: &str) -> Self { - let producer: FutureProducer = ClientConfig::new() - .set("bootstrap.servers", brokers) - .set("message.timeout.ms", "5000") - .create() - .expect("Kafka producer creation failed"); - Self { - producer, - topic: topic.to_string(), - } - } -} - -#[cfg(feature = "audit-kafka")] -impl AuditTarget for KafkaAuditTarget { - fn send(&self, entry: AuditEntry) { - let topic = self.topic.clone(); - let span_id = entry.span_id.clone(); - let payload = serde_json::to_string(&entry).unwrap(); - // let record = FutureRecord::to(&topic).payload(&payload).key(&span_id); - tokio::spawn({ - // 在异步闭包内部创建 record - let topic = topic; - let payload = payload; - let span_id = span_id; - let producer = self.producer.clone(); - async move { - let record = FutureRecord::to(&topic).payload(&payload).key(&span_id); - if let Err(e) = producer.send(record, std::time::Duration::from_secs(0)).await { - eprintln!("Failed to send to Kafka: {:?}", e); - } - } - }); - } -} -/// AuditLogger is a logger that logs audit entries -/// to multiple targets -/// -/// # Example -/// ``` -/// use rustfs_obs::{AuditEntry, AuditLogger, FileAuditTarget}; -/// -/// #[tokio::main] -/// async fn main() { -/// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); -/// let entry = AuditEntry { -/// version: "1.0".to_string(), -/// event_type: "read".to_string(), -/// bucket: "bucket".to_string(), -/// object: "object".to_string(), -/// user: "user".to_string(), -/// time: "time".to_string(), -/// user_agent: "user_agent".to_string(), -/// span_id: "span_id".to_string(), -/// }; -/// logger.log(entry).await; -/// } -/// ``` - -#[derive(Debug)] -/// AuditLogger is a logger that logs audit entries -/// to multiple targets -/// # Example -/// ``` -/// use rustfs_obs::{AuditEntry, AuditLogger, FileAuditTarget}; -/// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); -/// ``` -/// # Note -/// This feature requires the `tokio` crate -/// # Example -/// ```toml -/// [dependencies] -/// tokio = { version = "1", features = ["full"] } -/// rustfs_obs = { version = "0.1.0"} -/// ``` -/// # Note -/// This feature requires the `serde` crate -/// # Example -/// ```toml -/// [dependencies] -/// serde = { version = "1", features = ["derive"] } -/// rustfs_obs = { version = "0.1.0"} -/// ``` -pub struct AuditLogger { - tx: mpsc::Sender, -} - -impl AuditLogger { - /// Create a new AuditLogger with the given targets - /// that will receive audit entries - /// # Arguments - /// * `targets` - A vector of audit targets - /// # Returns - /// * An AuditLogger - /// # Example - /// ``` - /// use rustfs_obs::{AuditLogger, AuditEntry, FileAuditTarget}; - /// - /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); - /// ``` - pub fn new(targets: Vec>) -> Self { - let (tx, mut rx) = mpsc::channel::(1000); - tokio::spawn(async move { - while let Some(entry) = rx.recv().await { - for target in &targets { - target.send(entry.clone()); - } - } - }); - Self { tx } - } - - /// Log an audit entry - /// # Arguments - /// * `entry` - The audit entry to log - /// # Example - /// ``` - /// use rustfs_obs::{AuditEntry, AuditLogger, FileAuditTarget}; - /// - /// #[tokio::main] - /// async fn main() { - /// let logger = AuditLogger::new(vec![Box::new(FileAuditTarget)]); - /// let entry = AuditEntry { - /// version: "1.0".to_string(), - /// event_type: "read".to_string(), - /// bucket: "bucket".to_string(), - /// object: "object".to_string(), - /// user: "user".to_string(), - /// time: "time".to_string(), - /// user_agent: "user_agent".to_string(), - /// span_id: "span_id".to_string(), - /// }; - /// logger.log(entry).await; - /// } - /// ``` - pub async fn log(&self, entry: AuditEntry) { - // 将日志消息记录到当前 Span - tracing::Span::current() - .record("log_message", &entry.bucket) - .record("source", &entry.event_type); - let _ = self.tx.send(entry).await; - } -} diff --git a/packages/obs/src/config.rs b/packages/obs/src/config.rs new file mode 100644 index 000000000..8b14f7d0d --- /dev/null +++ b/packages/obs/src/config.rs @@ -0,0 +1,90 @@ +use config::{Config, File, FileFormat}; +use serde::Deserialize; +use std::env; + +/// OpenTelemetry Configuration +#[derive(Debug, Deserialize, Clone, Default)] +pub struct OtelConfig { + pub endpoint: String, + pub use_stdout: bool, + pub sample_ratio: f64, + pub meter_interval: u64, + pub service_name: String, + pub service_version: String, + pub deployment_environment: String, +} + +/// Kafka Sink Configuration - Add batch parameters +#[derive(Debug, Deserialize, Clone)] +pub struct KafkaSinkConfig { + pub enabled: bool, + pub bootstrap_servers: String, + pub topic: String, + pub batch_size: Option, // Batch size, default 100 + pub batch_timeout_ms: Option, // Batch timeout time, default 1000ms +} + +/// Webhook Sink Configuration - Add Retry Parameters +#[derive(Debug, Deserialize, Clone)] +pub struct WebhookSinkConfig { + pub enabled: bool, + pub url: String, + pub max_retries: Option, // Maximum number of retry times, default 3 + pub retry_delay_ms: Option, // Retry the delay cardinality, default 100ms +} + +/// File Sink Configuration - Add buffering parameters +#[derive(Debug, Deserialize, Clone)] +pub struct FileSinkConfig { + pub enabled: bool, + pub path: String, + pub buffer_size: Option, // Write buffer size, default 8192 + pub flush_interval_ms: Option, // Refresh interval time, default 1000ms + pub flush_threshold: Option, // Refresh threshold, default 100 logs +} + +/// Sink configuration collection +#[derive(Debug, Deserialize, Clone)] +pub struct SinkConfig { + pub kafka: KafkaSinkConfig, + pub webhook: WebhookSinkConfig, + pub file: FileSinkConfig, +} + +///Logger Configuration +#[derive(Debug, Deserialize, Clone)] +pub struct LoggerConfig { + pub queue_capacity: Option, +} + +/// Overall application configuration +#[derive(Debug, Deserialize, Clone)] +pub struct AppConfig { + pub observability: OtelConfig, + pub sinks: SinkConfig, + pub logger: LoggerConfig, +} + +/// Loading the configuration file +/// Supports TOML, YAML and .env formats, read in order by priority +pub fn load_config(config_dir: Option) -> AppConfig { + let config_dir = config_dir.unwrap_or_else(|| { + env::current_dir() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|_| { + eprintln!("Warning: Failed to get current directory, using empty path"); + String::new() + }) + }); + + println!("config_dir: {}", config_dir); + + let config = Config::builder() + .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml)) + .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Yaml).required(false)) + .add_source(config::Environment::with_prefix("")) + .build() + .unwrap(); + + config.try_deserialize().unwrap() +} diff --git a/packages/obs/src/entry.rs b/packages/obs/src/entry.rs new file mode 100644 index 000000000..a8cef168a --- /dev/null +++ b/packages/obs/src/entry.rs @@ -0,0 +1,80 @@ +use chrono::{DateTime, Utc}; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use tracing_core::Level; + +/// Wrapper for `tracing_core::Level` to implement `Serialize` and `Deserialize` +#[derive(Debug, Clone)] +pub struct SerializableLevel(pub Level); + +impl From for SerializableLevel { + fn from(level: Level) -> Self { + SerializableLevel(level) + } +} + +impl From for Level { + fn from(serializable_level: SerializableLevel) -> Self { + serializable_level.0 + } +} + +impl Serialize for SerializableLevel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} + +impl<'de> Deserialize<'de> for SerializableLevel { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "TRACE" => Ok(SerializableLevel(Level::TRACE)), + "DEBUG" => Ok(SerializableLevel(Level::DEBUG)), + "INFO" => Ok(SerializableLevel(Level::INFO)), + "WARN" => Ok(SerializableLevel(Level::WARN)), + "ERROR" => Ok(SerializableLevel(Level::ERROR)), + _ => Err(D::Error::custom("unknown log level")), + } + } +} + +/// Server log entry structure +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LogEntry { + pub timestamp: DateTime, // Log timestamp + pub level: SerializableLevel, // Log Level + pub message: String, // Log messages + pub source: String, // Log source (such as module name) + pub request_id: Option, // Request ID (Common Server Fields) + pub user_id: Option, // User ID (Common Server Fields) + pub fields: Vec<(String, String)>, // Attached fields (key value pairs) +} + +impl LogEntry { + /// Create a new LogEntry + pub fn new( + level: Level, + message: String, + source: String, + request_id: Option, + user_id: Option, + fields: Vec<(String, String)>, + ) -> Self { + LogEntry { + timestamp: Utc::now(), + level: SerializableLevel::from(level), + message, + source, + request_id, + user_id, + fields, + } + } +} diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index caf993220..469547d61 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -1,190 +1,32 @@ -//! Logging utilities - +/// # obs /// -/// This crate provides utilities for logging. -/// -/// # Examples -/// ``` -/// use rustfs_obs::{log_info, log_error}; -/// -/// log_info("This is an informational message"); -/// log_error("This is an error message"); -/// ``` -#[cfg(feature = "audit-kafka")] -pub use audit::KafkaAuditTarget; -#[cfg(feature = "audit-webhook")] -pub use audit::WebhookAuditTarget; -pub use audit::{AuditEntry, AuditLogger, AuditTarget, FileAuditTarget}; -pub use logger::{log_debug, log_error, log_info}; -pub use telemetry::Telemetry; - -mod audit; +/// `obs` is a logging and observability library for Rust. +/// It provides a simple and easy-to-use interface for logging and observability. +/// It is built on top of the `log` crate and `opentelemetry` crate. +mod config; +mod entry; mod logger; +mod sink; mod telemetry; +mod utils; +mod worker; -#[cfg(test)] -mod tests { - use crate::{log_info, AuditEntry, AuditLogger, AuditTarget, FileAuditTarget, Telemetry}; - use chrono::Utc; - use opentelemetry::global; - use opentelemetry::trace::{TraceContextExt, Tracer}; - use std::time::{Duration, SystemTime}; - use tracing::{instrument, Span}; - use tracing_opentelemetry::OpenTelemetrySpanExt; +pub use config::load_config; +pub use config::{AppConfig, OtelConfig}; +pub use entry::{LogEntry, SerializableLevel}; +pub use logger::start_logger; +pub use logger::{LogError, Logger}; +pub use sink::Sink; +pub use telemetry::init_telemetry; +pub use utils::{get_local_ip, get_local_ip_with_default}; +pub use worker::start_worker; - #[instrument(fields(bucket, object, user))] - async fn put_object(audit_logger: &AuditLogger, bucket: String, object: String, user: String) { - let start_time = SystemTime::now(); - log_info("Starting PUT operation"); - - // Simulate the operation - tokio::time::sleep(Duration::from_millis(100)).await; - - // Record Metrics - let meter = global::meter("rustfs.rs"); - let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); - request_duration.record( - start_time.elapsed().unwrap().as_secs_f64(), - &[opentelemetry::KeyValue::new("operation", "put_object")], - ); - - // Gets the current span - let span = Span::current(); - - // Use 'OpenTelemetrySpanExt' to get 'SpanContext' - let span_context = span.context(); // Get context via OpenTelemetrySpanExt - let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId - - // Audit events are logged - let audit_entry = AuditEntry { - version: "1.0".to_string(), - event_type: "s3_put_object".to_string(), - bucket, - object, - user, - time: Utc::now().to_rfc3339(), - user_agent: "rustfs.rs-client".to_string(), - span_id, - }; - audit_logger.log(audit_entry).await; - - log_info("PUT operation completed"); - } - - #[tokio::test] - // #[cfg(feature = "audit-webhook")] - // #[cfg(feature = "audit-kafka")] - async fn test_main() { - let telemetry = Telemetry::init(); - - // Initialize multiple audit objectives - let audit_targets: Vec> = vec![ - Box::new(FileAuditTarget), - // Box::new(KafkaAuditTarget::new("localhost:9092", "rustfs-audit")), - // Box::new(WebhookAuditTarget::new("http://localhost:8080/audit")), - ]; - let audit_logger = AuditLogger::new(audit_targets); - - // 创建根 Span 并执行操作 - // let tracer = global::tracer("main"); - // tracer.in_span("main_operation", |cx| { - // Span::current().set_parent(cx); - // log_info("Starting test async"); - // tokio::runtime::Runtime::new().unwrap().block_on(async { - log_info("Starting test"); - // Test the PUT operation - put_object(&audit_logger, "my-bucket".to_string(), "my-object.txt".to_string(), "user123".to_string()).await; - tokio::time::sleep(Duration::from_millis(100)).await; - query_object(&audit_logger, "my-bucket".to_string(), "my-object.txt".to_string(), "user123".to_string()).await; - tokio::time::sleep(Duration::from_millis(100)).await; - for i in 0..100 { - put_object( - &audit_logger, - format!("my-bucket-{}", i), - format!("my-object-{}", i), - "user123".to_string(), - ) - .await; - tokio::time::sleep(Duration::from_millis(100)).await; - query_object( - &audit_logger, - format!("my-bucket-{}", i), - format!("my-object-{}", i), - "user123".to_string(), - ) - .await; - tokio::time::sleep(Duration::from_millis(100)).await; - } - - // Wait for the export to complete - tokio::time::sleep(Duration::from_secs(2)).await; - log_info("Test completed"); - // }); - // }); - drop(telemetry); // Make sure to clean up - } - - #[instrument(fields(bucket, object, user))] - async fn query_object(audit_logger: &AuditLogger, bucket: String, object: String, user: String) { - let start_time = SystemTime::now(); - log_info("Starting query operation"); - - // Simulate the operation - tokio::time::sleep(Duration::from_millis(100)).await; - - // Record Metrics - let meter = global::meter("rustfs.rs"); - let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); - request_duration.record( - start_time.elapsed().unwrap().as_secs_f64(), - &[opentelemetry::KeyValue::new("operation", "query_object")], - ); - - // Gets the current span - let span = Span::current(); - // Use 'OpenTelemetrySpanExt' to get 'SpanContext' - let span_context = span.context(); // Get context via OpenTelemetrySpanExt - let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId - query_one(user.clone()); - query_two(user.clone()); - query_three(user.clone()); - // Audit events are logged - let audit_entry = AuditEntry { - version: "1.0".to_string(), - event_type: "s3_query_object".to_string(), - bucket, - object, - user, - time: Utc::now().to_rfc3339(), - user_agent: "rustfs.rs-client".to_string(), - span_id, - }; - audit_logger.log(audit_entry).await; - - log_info("query operation completed"); - } - #[instrument(fields(user))] - fn query_one(user: String) { - // 初始化 OpenTelemetry Tracer - let tracer = global::tracer("query_one"); - tracer.in_span("doing_work", |cx| { - // Traced app logic here... - Span::current().set_parent(cx); - log_info("Doing work..."); - let current_span = Span::current(); - let span_context = current_span.context(); - let trace_id = span_context.clone().span().span_context().trace_id().to_string(); - let span_id = span_context.clone().span().span_context().span_id().to_string(); - log_info(format!("trace_id: {}, span_id: {}", trace_id, span_id).as_str()); - }); - log_info(format!("Starting query_one operation user:{}", user).as_str()); - } - #[instrument(fields(user))] - fn query_two(user: String) { - log_info(format!("Starting query_two operation user:{}", user).as_str()); - } - #[instrument(fields(user))] - fn query_three(user: String) { - log_info(format!("Starting query_three operation user: {}", user).as_str()); - } +/// Log module initialization function +/// +/// Return to Logger and Clean Guard +pub fn init_logging(config: AppConfig) -> (Logger, telemetry::OtelGuard) { + let guard = init_telemetry(&config.observability); + let sinks = sink::create_sinks(&config); + let logger = start_logger(&config, sinks); + (logger, guard) } diff --git a/packages/obs/src/logger.rs b/packages/obs/src/logger.rs index 023419592..f2297c9ef 100644 --- a/packages/obs/src/logger.rs +++ b/packages/obs/src/logger.rs @@ -1,46 +1,238 @@ -use tracing::{debug, error, info}; +use crate::{AppConfig, LogEntry, SerializableLevel, Sink}; +use std::sync::Arc; +use tokio::sync::mpsc::{self, Receiver, Sender}; -/// Log an info message -/// -/// # Arguments -/// msg: &str - The message to log -/// -/// # Example -/// ``` -/// use rustfs_obs::log_info; -/// -/// log_info("This is an info message"); -/// ``` -pub fn log_info(msg: &str) { - info!("{}", msg); +/// Server log processor +pub struct Logger { + sender: Sender, // Log sending channel + queue_capacity: usize, } -/// Log an error message -/// -/// # Arguments -/// msg: &str - The message to log -/// -/// # Example -/// ``` -/// use rustfs_obs::log_error; -/// -/// log_error("This is an error message"); -/// ``` -pub fn log_error(msg: &str) { - error!("{}", msg); +impl Logger { + /// Create a new Logger instance + /// Returns Logger and corresponding Receiver + pub fn new(config: &AppConfig) -> (Self, Receiver) { + // Get queue capacity from configuration, or use default values 10000 + let queue_capacity = config.logger.queue_capacity.unwrap_or(10000); + let (sender, receiver) = mpsc::channel(queue_capacity); + ( + Logger { + sender, + queue_capacity, + }, + receiver, + ) + } + + // Add a method to get queue capacity + pub fn queue_capacity(&self) -> usize { + self.queue_capacity + } + + /// Asynchronous logging of server logs + /// Attach the log to the current Span and generate a separate Tracing Event + #[tracing::instrument(skip(self), fields(log_source = "logger"))] + pub async fn log(&self, entry: LogEntry) -> Result<(), LogError> { + // Log messages to the current Span + tracing::Span::current() + .record("log_message", &entry.message) + .record("source", &entry.source); + + // Record queue utilization (if a certain threshold is exceeded) + let queue_len = self.sender.capacity(); + let utilization = queue_len as f64 / self.queue_capacity as f64; + if utilization > 0.8 { + tracing::warn!("Log queue utilization high: {:.1}%", utilization * 100.0); + } + + // Generate independent Tracing Events with full LogEntry information + // Generate corresponding events according to level + match entry.level { + SerializableLevel(tracing::Level::ERROR) => { + tracing::error!( + target: "server_logs", + timestamp = %entry.timestamp, + message = %entry.message, + source = %entry.source, + request_id = ?entry.request_id, + user_id = ?entry.user_id, + fields = ?entry.fields + ); + } + SerializableLevel(tracing::Level::WARN) => { + tracing::warn!( + target: "server_logs", + timestamp = %entry.timestamp, + message = %entry.message, + source = %entry.source, + request_id = ?entry.request_id, + user_id = ?entry.user_id, + fields = ?entry.fields + ); + } + SerializableLevel(tracing::Level::INFO) => { + tracing::info!( + target: "server_logs", + timestamp = %entry.timestamp, + message = %entry.message, + source = %entry.source, + request_id = ?entry.request_id, + user_id = ?entry.user_id, + fields = ?entry.fields + ); + } + SerializableLevel(tracing::Level::DEBUG) => { + tracing::debug!( + target: "server_logs", + timestamp = %entry.timestamp, + message = %entry.message, + source = %entry.source, + request_id = ?entry.request_id, + user_id = ?entry.user_id, + fields = ?entry.fields + ); + } + SerializableLevel(tracing::Level::TRACE) => { + tracing::trace!( + target: "server_logs", + timestamp = %entry.timestamp, + message = %entry.message, + source = %entry.source, + request_id = ?entry.request_id, + user_id = ?entry.user_id, + fields = ?entry.fields + ); + } + } + + // Send logs to asynchronous queues to improve error handling + match self.sender.try_send(entry) { + Ok(_) => Ok(()), + Err(mpsc::error::TrySendError::Full(entry)) => { + // Processing strategy when queue is full + tracing::warn!("Log queue full, applying backpressure"); + match tokio::time::timeout( + std::time::Duration::from_millis(500), + self.sender.send(entry), + ) + .await + { + Ok(Ok(_)) => Ok(()), + Ok(Err(_)) => Err(LogError::SendFailed("Channel closed")), + Err(_) => Err(LogError::Timeout("Queue backpressure timeout")), + } + } + Err(mpsc::error::TrySendError::Closed(_)) => { + Err(LogError::SendFailed("Logger channel closed")) + } + } + } + + // Add convenient methods to simplify logging + // Fix the info() method, replacing None with an empty vector instead of the Option type + pub async fn info(&self, message: &str, source: &str) -> Result<(), LogError> { + self.log(LogEntry::new( + tracing::Level::INFO, + message.to_string(), + source.to_string(), + None, + None, + Vec::new(), // 使用空向量代替 None + )) + .await + } + + /// Add warn() method + pub async fn error(&self, message: &str, source: &str) -> Result<(), LogError> { + self.log(LogEntry::new( + tracing::Level::ERROR, + message.to_string(), + source.to_string(), + None, + None, + Vec::new(), + )) + .await + } + + /// Add warn() method + pub async fn warn(&self, message: &str, source: &str) -> Result<(), LogError> { + self.log(LogEntry::new( + tracing::Level::WARN, + message.to_string(), + source.to_string(), + None, + None, + Vec::new(), + )) + .await + } + + /// Add debug() method + pub async fn debug(&self, message: &str, source: &str) -> Result<(), LogError> { + self.log(LogEntry::new( + tracing::Level::DEBUG, + message.to_string(), + source.to_string(), + None, + None, + Vec::new(), + )) + .await + } + + /// Add trace() method + pub async fn trace(&self, message: &str, source: &str) -> Result<(), LogError> { + self.log(LogEntry::new( + tracing::Level::TRACE, + message.to_string(), + source.to_string(), + None, + None, + Vec::new(), + )) + .await + } + + // Add extension methods with context information for more flexibility + pub async fn info_with_context( + &self, + message: &str, + source: &str, + request_id: Option, + user_id: Option, + fields: Vec<(String, String)>, + ) -> Result<(), LogError> { + self.log(LogEntry::new( + tracing::Level::INFO, + message.to_string(), + source.to_string(), + request_id, + user_id, + fields, + )) + .await + } + + // Add elegant closing method + pub async fn shutdown(self) -> Result<(), LogError> { + drop(self.sender); //Close the sending end so that the receiver knows that there is no new message + Ok(()) + } } -/// Log a debug message -/// -/// # Arguments -/// msg: &str - The message to log -/// -/// # Example -/// ``` -/// use rustfs_obs::log_debug; -/// -/// log_debug("This is a debug message"); -/// ``` -pub fn log_debug(msg: &str) { - debug!("{}", msg); +// Define custom error type +#[derive(Debug, thiserror::Error)] +pub enum LogError { + #[error("Failed to send log: {0}")] + SendFailed(&'static str), + #[error("Operation timed out: {0}")] + Timeout(&'static str), +} + +/// Start the log module +pub fn start_logger(config: &AppConfig, sinks: Vec>) -> Logger { + let (logger, receiver) = Logger::new(config); + tokio::spawn(crate::worker::start_worker(receiver, sinks)); + logger } diff --git a/packages/obs/src/sink.rs b/packages/obs/src/sink.rs new file mode 100644 index 000000000..37785cfba --- /dev/null +++ b/packages/obs/src/sink.rs @@ -0,0 +1,450 @@ +use crate::{AppConfig, LogEntry}; +use async_trait::async_trait; +use std::sync::Arc; +use tokio::fs::OpenOptions; +use tokio::io; +use tokio::io::AsyncWriteExt; + +/// Sink Trait definition, asynchronously write logs +#[async_trait] +pub trait Sink: Send + Sync { + async fn write(&self, entry: &LogEntry); +} + +#[cfg(feature = "kafka")] +/// Kafka Sink Implementation +pub struct KafkaSink { + producer: rdkafka::producer::FutureProducer, + topic: String, + batch_size: usize, + batch_timeout_ms: u64, + entries: Arc>>, + last_flush: Arc, +} + +#[cfg(feature = "kafka")] +impl KafkaSink { + /// Create a new KafkaSink instance + pub fn new(producer: rdkafka::producer::FutureProducer, topic: String, batch_size: usize, batch_timeout_ms: u64) -> Self { + // Create Arc-wrapped values first + let entries = Arc::new(tokio::sync::Mutex::new(Vec::with_capacity(batch_size))); + let last_flush = Arc::new(std::sync::atomic::AtomicU64::new( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64, + )); + let sink = KafkaSink { + producer: producer.clone(), + topic: topic.clone(), + batch_size, + batch_timeout_ms, + entries: entries.clone(), + last_flush: last_flush.clone(), + }; + + // Start background flusher + tokio::spawn(Self::periodic_flush(producer, topic, entries, last_flush, batch_timeout_ms)); + + sink + } + + /// Add a getter method to read the batch_timeout_ms field + #[allow(dead_code)] + pub fn batch_timeout(&self) -> u64 { + self.batch_timeout_ms + } + + /// Add a method to dynamically adjust the timeout if needed + #[allow(dead_code)] + pub fn set_batch_timeout(&mut self, new_timeout_ms: u64) { + self.batch_timeout_ms = new_timeout_ms; + } + + async fn periodic_flush( + producer: rdkafka::producer::FutureProducer, + topic: String, + entries: Arc>>, + last_flush: Arc, + timeout_ms: u64, + ) { + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(timeout_ms / 2)).await; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + let last = last_flush.load(std::sync::atomic::Ordering::Relaxed); + + if now - last >= timeout_ms { + let mut batch = entries.lock().await; + if !batch.is_empty() { + Self::send_batch(&producer, &topic, batch.drain(..).collect()).await; + last_flush.store(now, std::sync::atomic::Ordering::Relaxed); + } + } + } + } + + async fn send_batch(producer: &rdkafka::producer::FutureProducer, topic: &str, entries: Vec) { + for entry in entries { + let payload = match serde_json::to_string(&entry) { + Ok(p) => p, + Err(e) => { + eprintln!("Failed to serialize log entry: {}", e); + continue; + } + }; + + let span_id = entry.timestamp.to_rfc3339(); + + let _ = producer + .send( + rdkafka::producer::FutureRecord::to(topic).payload(&payload).key(&span_id), + std::time::Duration::from_secs(5), + ) + .await; + } + } +} + +#[cfg(feature = "kafka")] +#[async_trait] +impl Sink for KafkaSink { + async fn write(&self, entry: &LogEntry) { + let mut batch = self.entries.lock().await; + batch.push(entry.clone()); + + let should_flush_by_size = batch.len() >= self.batch_size; + let should_flush_by_time = { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let last = self.last_flush.load(std::sync::atomic::Ordering::Relaxed); + now - last >= self.batch_timeout_ms + }; + + if should_flush_by_size || should_flush_by_time { + // Existing flush logic + let entries_to_send: Vec = batch.drain(..).collect(); + let producer = self.producer.clone(); + let topic = self.topic.clone(); + + self.last_flush.store( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64, + std::sync::atomic::Ordering::Relaxed, + ); + + tokio::spawn(async move { + KafkaSink::send_batch(&producer, &topic, entries_to_send).await; + }); + } + } +} + +#[cfg(feature = "kafka")] +impl Drop for KafkaSink { + fn drop(&mut self) { + // Perform any necessary cleanup here + // For example, you might want to flush any remaining entries + let producer = self.producer.clone(); + let topic = self.topic.clone(); + let entries = self.entries.clone(); + let last_flush = self.last_flush.clone(); + + tokio::spawn(async move { + let mut batch = entries.lock().await; + if !batch.is_empty() { + KafkaSink::send_batch(&producer, &topic, batch.drain(..).collect()).await; + last_flush.store( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64, + std::sync::atomic::Ordering::Relaxed, + ); + } + }); + + eprintln!("Dropping KafkaSink with topic: {}", self.topic); + } +} + +#[cfg(feature = "webhook")] +/// Webhook Sink Implementation +pub struct WebhookSink { + url: String, + client: reqwest::Client, + max_retries: usize, + retry_delay_ms: u64, +} + +#[cfg(feature = "webhook")] +impl WebhookSink { + pub fn new(url: String, max_retries: usize, retry_delay_ms: u64) -> Self { + WebhookSink { + url, + client: reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()), + max_retries, + retry_delay_ms, + } + } +} + +#[cfg(feature = "webhook")] +#[async_trait] +impl Sink for WebhookSink { + async fn write(&self, entry: &LogEntry) { + let mut retries = 0; + let url = self.url.clone(); + let entry_clone = entry.clone(); + + while retries < self.max_retries { + match self.client.post(&url).json(&entry_clone).send().await { + Ok(response) if response.status().is_success() => { + return; + } + _ => { + retries += 1; + if retries < self.max_retries { + tokio::time::sleep(tokio::time::Duration::from_millis( + self.retry_delay_ms * (1 << retries), // Exponential backoff + )) + .await; + } + } + } + } + + eprintln!("Failed to send log to webhook after {} retries", self.max_retries); + } +} + +#[cfg(feature = "webhook")] +impl Drop for WebhookSink { + fn drop(&mut self) { + // Perform any necessary cleanup here + // For example, you might want to log that the sink is being dropped + eprintln!("Dropping WebhookSink with URL: {}", self.url); + } +} + +#[cfg(feature = "file")] +/// File Sink Implementation +pub struct FileSink { + path: String, + buffer_size: usize, + writer: Arc>>, + entry_count: std::sync::atomic::AtomicUsize, + last_flush: std::sync::atomic::AtomicU64, + flush_interval_ms: u64, // Time between flushes + flush_threshold: usize, // Number of entries before flush +} + +#[cfg(feature = "file")] +impl FileSink { + #[allow(dead_code)] + pub async fn new( + path: String, + buffer_size: usize, + flush_interval_ms: u64, + flush_threshold: usize, + ) -> Result { + let file = OpenOptions::new().append(true).create(true).open(&path).await?; + + let writer = tokio::io::BufWriter::with_capacity(buffer_size, file); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + Ok(FileSink { + path, + buffer_size, + writer: Arc::new(tokio::sync::Mutex::new(writer)), + entry_count: std::sync::atomic::AtomicUsize::new(0), + last_flush: std::sync::atomic::AtomicU64::new(now), + flush_interval_ms, + flush_threshold, + }) + } + + #[allow(dead_code)] + async fn initialize_writer(&mut self) -> io::Result<()> { + let file = tokio::fs::File::create(&self.path).await?; + + // Use buffer_size to create a buffer writer with a specified capacity + let buf_writer = io::BufWriter::with_capacity(self.buffer_size, file); + + // Replace the original writer with the new Mutex + self.writer = Arc::new(tokio::sync::Mutex::new(buf_writer)); + Ok(()) + } + + // Get the current buffer size + #[allow(dead_code)] + pub fn buffer_size(&self) -> usize { + self.buffer_size + } + + // How to dynamically adjust the buffer size + #[allow(dead_code)] + pub async fn set_buffer_size(&mut self, new_size: usize) -> io::Result<()> { + if self.buffer_size != new_size { + self.buffer_size = new_size; + // Reinitialize the writer directly, without checking is_some() + self.initialize_writer().await?; + } + Ok(()) + } + + // Check if flushing is needed based on count or time + fn should_flush(&self) -> bool { + // Check entry count threshold + if self.entry_count.load(std::sync::atomic::Ordering::Relaxed) >= self.flush_threshold { + return true; + } + + // Check time threshold + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + let last = self.last_flush.load(std::sync::atomic::Ordering::Relaxed); + now - last >= self.flush_interval_ms + } +} + +#[cfg(feature = "file")] +#[async_trait] +impl Sink for FileSink { + async fn write(&self, entry: &LogEntry) { + let line = format!("{:?}\n", entry); + let mut writer = self.writer.lock().await; + + if let Err(e) = writer.write_all(line.as_bytes()).await { + eprintln!("Failed to write log to file {}: {}", self.path, e); + return; + } + + // Only flush periodically to improve performance + // Logic to determine when to flush could be added here + // Increment the entry count + self.entry_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + // Check if we should flush + if self.should_flush() { + if let Err(e) = writer.flush().await { + eprintln!("Failed to flush log file {}: {}", self.path, e); + return; + } + + // Reset counters + self.entry_count.store(0, std::sync::atomic::Ordering::Relaxed); + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + self.last_flush.store(now, std::sync::atomic::Ordering::Relaxed); + } + } +} + +#[cfg(feature = "file")] +impl Drop for FileSink { + fn drop(&mut self) { + let writer = self.writer.clone(); + let path = self.path.clone(); + + tokio::task::spawn_blocking(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let mut writer = writer.lock().await; + if let Err(e) = writer.flush().await { + eprintln!("Failed to flush log file {}: {}", path, e); + } + }); + }); + } +} + +/// Create a list of Sink instances +pub fn create_sinks(config: &AppConfig) -> Vec> { + let mut sinks: Vec> = Vec::new(); + + #[cfg(feature = "kafka")] + if config.sinks.kafka.enabled { + match rdkafka::config::ClientConfig::new() + .set("bootstrap.servers", &config.sinks.kafka.bootstrap_servers) + .set("message.timeout.ms", "5000") + .create() + { + Ok(producer) => { + sinks.push(Arc::new(KafkaSink::new( + producer, + config.sinks.kafka.topic.clone(), + config.sinks.kafka.batch_size.unwrap_or(100), + config.sinks.kafka.batch_timeout_ms.unwrap_or(1000), + ))); + } + Err(e) => eprintln!("Failed to create Kafka producer: {}", e), + } + } + + #[cfg(feature = "webhook")] + if config.sinks.webhook.enabled { + sinks.push(Arc::new(WebhookSink::new( + config.sinks.webhook.url.clone(), + config.sinks.webhook.max_retries.unwrap_or(3), + config.sinks.webhook.retry_delay_ms.unwrap_or(100), + ))); + } + + #[cfg(feature = "file")] + { + let path = if config.sinks.file.enabled { + config.sinks.file.path.clone() + } else { + "default.log".to_string() + }; + + // Use synchronous file operations + let file_result = std::fs::OpenOptions::new().append(true).create(true).open(&path); + + match file_result { + Ok(file) => { + let buffer_size = config.sinks.file.buffer_size.unwrap_or(8192); + let writer = tokio::io::BufWriter::with_capacity(buffer_size, tokio::fs::File::from_std(file)); + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + sinks.push(Arc::new(FileSink { + path: path.clone(), + buffer_size, + writer: Arc::new(tokio::sync::Mutex::new(writer)), + entry_count: std::sync::atomic::AtomicUsize::new(0), + last_flush: std::sync::atomic::AtomicU64::new(now), + flush_interval_ms: config.sinks.file.flush_interval_ms.unwrap_or(1000), + flush_threshold: config.sinks.file.flush_threshold.unwrap_or(100), + })); + } + Err(e) => eprintln!("Failed to create file sink: {}", e), + } + } + + sinks +} diff --git a/packages/obs/src/telemetry.rs b/packages/obs/src/telemetry.rs index 0eed5466d..fb1111303 100644 --- a/packages/obs/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -1,149 +1,222 @@ +use crate::{get_local_ip_with_default, OtelConfig}; use opentelemetry::trace::TracerProvider; use opentelemetry::{global, KeyValue}; use opentelemetry_appender_tracing::layer; -use opentelemetry_otlp::{self, WithExportConfig}; +use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::logs::SdkLoggerProvider; use opentelemetry_sdk::{ metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}, trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, Resource, }; -use opentelemetry_semantic_conventions::attribute::NETWORK_LOCAL_ADDRESS; use opentelemetry_semantic_conventions::{ - attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION}, + attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_NAME, SERVICE_VERSION}, SCHEMA_URL, }; -use std::time::Duration; -use tracing::{info, Level}; use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; -/// Telemetry is a wrapper around the OpenTelemetry SDK Tracer and Meter providers. -/// It initializes the global Tracer and Meter providers, and sets up the tracing subscriber. -/// The Tracer and Meter providers are shut down when the Telemetry instance is dropped. -/// This is a convenience struct to ensure that the global providers are properly initialized and shut down. +/// A guard object that manages the lifecycle of OpenTelemetry components. +/// +/// This struct holds references to the created OpenTelemetry providers and ensures +/// they are properly shut down when the guard is dropped. It implements the RAII +/// (Resource Acquisition Is Initialization) pattern for managing telemetry resources. +/// +/// When this guard goes out of scope, it will automatically shut down: +/// - The tracer provider (for distributed tracing) +/// - The meter provider (for metrics collection) +/// - The logger provider (for structured logging) /// /// # Example -/// ``` -/// use rustfs_obs::Telemetry; /// -/// let _telemetry = Telemetry::init(); /// ``` -pub struct Telemetry { +/// use rustfs_obs::{init_telemetry, OtelConfig}; +/// +/// let config = OtelConfig::default(); +/// let otel_guard = init_telemetry(&config); +/// +/// // The guard is kept alive for the duration of the application +/// // When it's dropped, all telemetry components are properly shut down +/// drop(otel_guard); +/// ``` +pub struct OtelGuard { tracer_provider: SdkTracerProvider, meter_provider: SdkMeterProvider, + logger_provider: SdkLoggerProvider, } -impl Telemetry { - pub fn init() -> Self { - // Define service resource information - let resource = Resource::builder() - .with_service_name("rustfs-service") - .with_schema_url( - [ - KeyValue::new(SERVICE_NAME, "rustfs-service"), - KeyValue::new(SERVICE_VERSION, "0.1.0"), - KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, "develop"), - KeyValue::new(NETWORK_LOCAL_ADDRESS, "127.0.0.1"), - ], - SCHEMA_URL, - ) - .build(); +impl Drop for OtelGuard { + fn drop(&mut self) { + if let Err(err) = self.tracer_provider.shutdown() { + eprintln!("Tracer shutdown error: {:?}", err); + } + if let Err(err) = self.meter_provider.shutdown() { + eprintln!("Meter shutdown error: {:?}", err); + } + if let Err(err) = self.logger_provider.shutdown() { + eprintln!("Logger shutdown error: {:?}", err); + } + } +} - let tracer_exporter = opentelemetry_otlp::SpanExporter::builder() +/// create OpenTelemetry Resource +fn resource(config: &OtelConfig) -> Resource { + Resource::builder() + .with_service_name(config.service_name.clone()) + .with_schema_url( + [ + KeyValue::new(SERVICE_NAME, config.service_name.clone()), + KeyValue::new(SERVICE_VERSION, config.service_version.clone()), + KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, config.deployment_environment.clone()), + KeyValue::new(NETWORK_LOCAL_ADDRESS, get_local_ip_with_default()), + ], + SCHEMA_URL, + ) + .build() +} + +/// Initialize Meter Provider +fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { + let mut builder = MeterProviderBuilder::default().with_resource(resource(config)); + // If endpoint is empty, use stdout output + if config.endpoint.is_empty() { + builder = builder.with_reader( + PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()) + .with_interval(std::time::Duration::from_secs(config.meter_interval)) + .build(), + ); + } else { + // If endpoint is not empty, use otlp output + let exporter = opentelemetry_otlp::MetricExporter::builder() .with_tonic() - .with_endpoint("http://localhost:4317") - .with_protocol(opentelemetry_otlp::Protocol::Grpc) - .with_timeout(Duration::from_secs(3)) - .build() - .unwrap(); - - // Configure Tracer Provider - let tracer_provider = SdkTracerProvider::builder() - .with_sampler(Sampler::AlwaysOn) - .with_id_generator(RandomIdGenerator::default()) - .with_resource(resource.clone()) - .with_batch_exporter(tracer_exporter) - // .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) - .build(); - - let meter_exporter = opentelemetry_otlp::MetricExporter::builder() - .with_tonic() - .with_endpoint("http://localhost:4317") - .with_protocol(opentelemetry_otlp::Protocol::Grpc) - .with_timeout(Duration::from_secs(3)) + .with_endpoint(&config.endpoint) .with_temporality(opentelemetry_sdk::metrics::Temporality::default()) .build() .unwrap(); + builder = builder.with_reader( + PeriodicReader::builder(exporter) + .with_interval(std::time::Duration::from_secs(config.meter_interval)) + .build(), + ); + // If use_stdout is true, output to stdout at the same time + if config.use_stdout { + builder = builder.with_reader( + PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()) + .with_interval(std::time::Duration::from_secs(config.meter_interval)) + .build(), + ); + } + } - let meter_reader = PeriodicReader::builder(meter_exporter) - .with_interval(Duration::from_secs(30)) - .build(); + let meter_provider = builder.build(); + global::set_meter_provider(meter_provider.clone()); + meter_provider +} - // For debugging in development - // let meter_stdout_reader = PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build(); +/// Initialize Tracer Provider +fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { + let sampler = if config.sample_ratio > 0.0 && config.sample_ratio < 1.0 { + Sampler::TraceIdRatioBased(config.sample_ratio) + } else { + Sampler::AlwaysOn + }; + let builder = SdkTracerProvider::builder() + .with_sampler(sampler) + .with_id_generator(RandomIdGenerator::default()) + .with_resource(resource(config)); - // Configure Meter Provider - let meter_provider = MeterProviderBuilder::default() - .with_resource(resource.clone()) - .with_reader(meter_reader) - // .with_reader(meter_stdout_reader) - .build(); + let tracer_provider = if config.endpoint.is_empty() { + builder + .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + .build() + } else { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(&config.endpoint) + .build() + .unwrap(); + if config.use_stdout { + builder + .with_batch_exporter(exporter) + .with_batch_exporter(opentelemetry_stdout::SpanExporter::default()) + } else { + builder.with_batch_exporter(exporter) + } + .build() + }; - // Set global Tracer and Meter providers - global::set_tracer_provider(tracer_provider.clone()); - global::set_meter_provider(meter_provider.clone()); + global::set_tracer_provider(tracer_provider.clone()); + tracer_provider +} - let tracer = tracer_provider.tracer("rustfs-service"); +/// Initialize Telemetry +pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { + let tracer_provider = init_tracer_provider(config); + let meter_provider = init_meter_provider(config); + let tracer = tracer_provider.tracer(config.service_name.clone()); - // // let _stdout_exporter = opentelemetry_stdout::LogExporter::default(); - // let otlp_exporter = opentelemetry_otlp::LogExporter::builder() - // .with_tonic() - // .with_endpoint("http://localhost:4317") - // // .with_timeout(Duration::from_secs(3)) - // // .with_protocol(opentelemetry_otlp::Protocol::Grpc) - // .build() - // .unwrap(); - // let provider: SdkLoggerProvider = SdkLoggerProvider::builder() - // .with_resource(resource.clone()) - // .with_simple_exporter(otlp_exporter) - // .build(); - // let filter_otel = EnvFilter::new("debug") - // // .add_directive("hyper=off".parse().unwrap()) - // // .add_directive("opentelemetry=off".parse().unwrap()) - // // .add_directive("tonic=off".parse().unwrap()) - // // .add_directive("h2=off".parse().unwrap()) - // .add_directive("reqwest=off".parse().unwrap()); - // let otel_layer = layer::OpenTelemetryTracingBridge::new(&provider).with_filter(filter_otel); + let logger_provider = if config.endpoint.is_empty() { + SdkLoggerProvider::builder() + .with_resource(resource(config)) + .with_simple_exporter(opentelemetry_stdout::LogExporter::default()) + .build() + } else { + let exporter = opentelemetry_otlp::LogExporter::builder() + .with_tonic() + .with_endpoint(&config.endpoint) + .build() + .unwrap(); + SdkLoggerProvider::builder() + .with_resource(resource(config)) + .with_batch_exporter(exporter) + .with_batch_exporter(opentelemetry_stdout::LogExporter::default()) + .build() + }; + + let otel_layer = layer::OpenTelemetryTracingBridge::new(&logger_provider); + // For the OpenTelemetry layer, add a tracing filter to filter events from + // OpenTelemetry and its dependent crates (opentelemetry-otlp uses crates + // like reqwest/tonic etc.) from being sent back to OTel itself, thus + // preventing infinite telemetry generation. The filter levels are set as + // follows: + // - Allow `info` level and above by default. + // - Restrict `opentelemetry`, `hyper`, `tonic`, and `reqwest` completely. + // Note: This will also drop events from crates like `tonic` etc. even when + // they are used outside the OTLP Exporter. For more details, see: + // https://github.com/open-telemetry/opentelemetry-rust/issues/761 + let filter_otel = EnvFilter::new("info") + .add_directive("hyper=off".parse().unwrap()) + .add_directive("opentelemetry=off".parse().unwrap()) + .add_directive("tonic=off".parse().unwrap()) + .add_directive("h2=off".parse().unwrap()) + .add_directive("reqwest=off".parse().unwrap()); + let otel_layer = otel_layer.with_filter(filter_otel); + let registry = tracing_subscriber::registry() + .with(tracing_subscriber::filter::LevelFilter::INFO) + .with(OpenTelemetryLayer::new(tracer)) + .with(MetricsLayer::new(meter_provider.clone())) + .with(otel_layer); + if config.endpoint.is_empty() { + // Create a new tracing::Fmt layer to print the logs to stdout. It has a + // default filter of `info` level and above, and `debug` and above for logs + // from OpenTelemetry crates. The filter levels can be customized as needed. let filter_fmt = EnvFilter::new("info").add_directive("opentelemetry=debug".parse().unwrap()); let fmt_layer = tracing_subscriber::fmt::layer() .with_thread_names(true) .with_filter(filter_fmt); - // Configure `tracing subscriber` - tracing_subscriber::registry() - .with(tracing_subscriber::filter::LevelFilter::from_level(Level::DEBUG)) + registry .with(tracing_subscriber::fmt::layer().with_ansi(true)) - .with(MetricsLayer::new(meter_provider.clone())) - .with(OpenTelemetryLayer::new(tracer)) - // .with(otel_layer) .with(fmt_layer) .init(); - info!(name: "my-event-name", target: "my-system", event_id = 20, user_name = "otel", user_email = "otel@opentelemetry.io", message = "This is an example message"); - Self { - tracer_provider, - meter_provider, - } + } else { + registry.with(tracing_subscriber::fmt::layer().with_ansi(false)).init(); + println!("Logs and meter,tracer enabled"); } -} -impl Drop for Telemetry { - fn drop(&mut self) { - if let Err(err) = self.tracer_provider.shutdown() { - eprintln!("{err:?}"); - } - if let Err(err) = self.meter_provider.shutdown() { - eprintln!("{err:?}"); - } + OtelGuard { + tracer_provider, + meter_provider, + logger_provider, } } diff --git a/packages/obs/src/utils.rs b/packages/obs/src/utils.rs new file mode 100644 index 000000000..774594577 --- /dev/null +++ b/packages/obs/src/utils.rs @@ -0,0 +1,42 @@ +use local_ip_address::{local_ip, local_ipv6}; +use std::net::{IpAddr, Ipv4Addr}; + +/// Get the IP address of the machine +/// +/// Priority is given to trying to get the IPv4 address, and if it fails, try to get the IPv6 address. +/// If both fail to retrieve, None is returned. +/// +/// # Returns +/// +/// * `Some(IpAddr)` - Native IP address (IPv4 or IPv6) +/// * `None` - Unable to obtain any native IP address +pub fn get_local_ip() -> Option { + local_ip().ok().or_else(|| local_ipv6().ok()) +} + +/// Get the IP address of the machine as a string +/// +/// If the IP address cannot be obtained, returns "127.0.0.1" as the default value. +/// +/// # Returns +/// +/// * `String` - Native IP address (IPv4 or IPv6) as a string, or the default value +pub fn get_local_ip_with_default() -> String { + get_local_ip() + .unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))) // Provide a safe default value + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_local_ip() { + match get_local_ip() { + Some(ip) => println!("the ip address of this machine:{}", ip), + None => println!("Unable to obtain the IP address of the machine"), + } + assert!(get_local_ip().is_some()); + } +} diff --git a/packages/obs/src/worker.rs b/packages/obs/src/worker.rs new file mode 100644 index 000000000..a04327945 --- /dev/null +++ b/packages/obs/src/worker.rs @@ -0,0 +1,13 @@ +use crate::{entry::LogEntry, sink::Sink}; +use std::sync::Arc; +use tokio::sync::mpsc::Receiver; + +/// Start the log processing worker thread +pub async fn start_worker(receiver: Receiver, sinks: Vec>) { + let mut receiver = receiver; + while let Some(entry) = receiver.recv().await { + for sink in &sinks { + sink.write(&entry).await; + } + } +} From c3ca7960e90a75eb3c2c08d5e3801e55ad365a27 Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 12 Mar 2025 11:58:10 +0800 Subject: [PATCH 07/27] improve dependencies feature --- packages/obs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/obs/Cargo.toml b/packages/obs/Cargo.toml index 8753d63c7..216e4296a 100644 --- a/packages/obs/Cargo.toml +++ b/packages/obs/Cargo.toml @@ -30,7 +30,7 @@ tracing = { workspace = true } tracing-core = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } -tokio = { workspace = true, features = ["sync", "fs"] } +tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread"] } rdkafka = { workspace = true, features = ["tokio"], optional = true } reqwest = { workspace = true, optional = true, default-features = false } serde_json = { workspace = true, optional = true } From ef162396cfb66776753258a80e37286e9f6058da Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 16 Mar 2025 16:33:52 +0800 Subject: [PATCH 08/27] improve code --- Cargo.lock | 1 + packages/obs/Cargo.toml | 5 +- packages/obs/examples/config.toml | 4 +- packages/obs/examples/server.rs | 69 +-- packages/obs/src/config.rs | 12 +- packages/obs/src/entry/audit.rs | 88 ++++ packages/obs/src/entry/base.rs | 114 +++++ packages/obs/src/{entry.rs => entry/log.rs} | 3 + packages/obs/src/entry/mod.rs | 3 + packages/obs/src/lib.rs | 43 +- packages/obs/src/logger.rs | 464 +++++++++++++++----- packages/obs/src/sink.rs | 7 +- packages/obs/src/telemetry.rs | 107 +++-- packages/obs/src/worker.rs | 2 +- rustfs/src/main.rs | 8 +- 15 files changed, 724 insertions(+), 206 deletions(-) create mode 100644 packages/obs/src/entry/audit.rs create mode 100644 packages/obs/src/entry/base.rs rename packages/obs/src/{entry.rs => entry/log.rs} (95%) create mode 100644 packages/obs/src/entry/mod.rs diff --git a/Cargo.lock b/Cargo.lock index ca81fe744..f619b7a52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5744,6 +5744,7 @@ dependencies = [ "tokio", "tracing", "tracing-core", + "tracing-error", "tracing-opentelemetry", "tracing-subscriber", ] diff --git a/packages/obs/Cargo.toml b/packages/obs/Cargo.toml index 216e4296a..86a9fb6c4 100644 --- a/packages/obs/Cargo.toml +++ b/packages/obs/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [features] default = ["file"] -kafka = ["dep:rdkafka", "dep:serde_json"] +kafka = ["dep:rdkafka"] webhook = ["dep:reqwest"] file = [] @@ -28,12 +28,13 @@ opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_ex serde = { workspace = true } tracing = { workspace = true } tracing-core = { workspace = true } +tracing-error = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread"] } rdkafka = { workspace = true, features = ["tokio"], optional = true } reqwest = { workspace = true, optional = true, default-features = false } -serde_json = { workspace = true, optional = true } +serde_json = { workspace = true } thiserror = { workspace = true } local-ip-address = { workspace = true } diff --git a/packages/obs/examples/config.toml b/packages/obs/examples/config.toml index 9c3c68a26..2ceac4397 100644 --- a/packages/obs/examples/config.toml +++ b/packages/obs/examples/config.toml @@ -3,7 +3,7 @@ endpoint = "http://localhost:4317" use_stdout = true sample_ratio = 0.5 meter_interval = 30 -service_name = "logging_service" +service_name = "rustfs_obs_service" service_version = "0.1.0" deployment_environment = "develop" @@ -23,7 +23,7 @@ batch_timeout_ms = 1000 # Default is 100ms if not specified [sinks.file] enabled = true -path = "app.log" +path = "logs/app.log" batch_size = 100 batch_timeout_ms = 1000 # Default is 8192 bytes if not specified diff --git a/packages/obs/examples/server.rs b/packages/obs/examples/server.rs index b2c104dd3..60b275de2 100644 --- a/packages/obs/examples/server.rs +++ b/packages/obs/examples/server.rs @@ -1,47 +1,53 @@ use opentelemetry::global; -use opentelemetry::trace::TraceContextExt; -use rustfs_obs::{init_logging, load_config, LogEntry}; +use rustfs_obs::{get_logger, init_obs, load_config, log_info, LogEntry}; use std::time::{Duration, SystemTime}; -use tracing::{info, instrument, Span}; +use tracing::{info, instrument}; use tracing_core::Level; -use tracing_opentelemetry::OpenTelemetrySpanExt; #[tokio::main] async fn main() { - let start_time = SystemTime::now(); let config = load_config(Some("packages/obs/examples/config".to_string())); - info!("Configuration file loading is complete {:?}", config.clone()); - let (logger, _guard) = init_logging(config); - info!("Log module initialization is completed"); + let (_logger, _guard) = init_obs(config.clone()).await; // Simulate the operation tokio::time::sleep(Duration::from_millis(100)).await; + run( + "service-demo".to_string(), + "object-demo".to_string(), + "user-demo".to_string(), + "service-demo".to_string(), + ) + .await; + info!("Program ends"); +} + +#[instrument(fields(bucket, object, user))] +async fn run(bucket: String, object: String, user: String, service_name: String) { + let start_time = SystemTime::now(); + info!("Log module initialization is completed service_name: {:?}", service_name); // Record Metrics let meter = global::meter("rustfs.rs"); let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); request_duration.record( start_time.elapsed().unwrap().as_secs_f64(), - &[opentelemetry::KeyValue::new("operation", "put_object")], + &[opentelemetry::KeyValue::new("operation", "run")], ); - // Gets the current span - let span = Span::current(); - // Use 'OpenTelemetrySpanExt' to get 'SpanContext' - let span_context = span.context(); // Get context via OpenTelemetrySpanExt - let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId - let trace_id = span_context.span().span_context().trace_id().to_string(); // Get the TraceId - let result = logger + let result = get_logger() + .lock() + .await .log(LogEntry::new( Level::INFO, "Process user requests".to_string(), "api_handler".to_string(), + Some("demo-audit".to_string()), Some("req-12345".to_string()), - Some("user-6789".to_string()), + Some(user), vec![ ("endpoint".to_string(), "/api/v1/data".to_string()), ("method".to_string(), "GET".to_string()), - ("span_id".to_string(), span_id), - ("trace_id".to_string(), trace_id), + ("bucket".to_string(), bucket), + ("object-length".to_string(), object.len().to_string()), ], )) .await; @@ -49,28 +55,31 @@ async fn main() { put_object("bucket".to_string(), "object".to_string(), "user".to_string()).await; info!("Logging is completed"); tokio::time::sleep(Duration::from_secs(2)).await; - info!("Program ends"); + info!("Program run ends"); } #[instrument(fields(bucket, object, user))] async fn put_object(bucket: String, object: String, user: String) { let start_time = SystemTime::now(); - info!("Starting PUT operation"); - // Gets the current span - let span = Span::current(); - // Use 'OpenTelemetrySpanExt' to get 'SpanContext' - let span_context = span.context(); // Get context via OpenTelemetrySpanExt - let span_id = span_context.span().span_context().span_id().to_string(); // Get the SpanId - let trace_id = span_context.span().span_context().trace_id().to_string(); // Get the TraceId + info!("Starting put_object operation"); + + let meter = global::meter("rustfs.rs"); + let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); + request_duration.record( + start_time.elapsed().unwrap().as_secs_f64(), + &[opentelemetry::KeyValue::new("operation", "put_object")], + ); + info!( - "Starting PUT operation content: bucket = {}, object = {}, user = {},span_id = {},trace_id = {},start_time = {}", + "Starting PUT operation content: bucket = {}, object = {}, user = {},start_time = {}", bucket, object, user, - span_id, - trace_id, start_time.elapsed().unwrap().as_secs_f64() ); + + let result = log_info("put_object logger info", "put_object").await; + info!("put_object is completed {:?}", result); // Simulate the operation tokio::time::sleep(Duration::from_millis(100)).await; diff --git a/packages/obs/src/config.rs b/packages/obs/src/config.rs index 8b14f7d0d..c6e57c421 100644 --- a/packages/obs/src/config.rs +++ b/packages/obs/src/config.rs @@ -15,7 +15,7 @@ pub struct OtelConfig { } /// Kafka Sink Configuration - Add batch parameters -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default)] pub struct KafkaSinkConfig { pub enabled: bool, pub bootstrap_servers: String, @@ -25,7 +25,7 @@ pub struct KafkaSinkConfig { } /// Webhook Sink Configuration - Add Retry Parameters -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default)] pub struct WebhookSinkConfig { pub enabled: bool, pub url: String, @@ -34,7 +34,7 @@ pub struct WebhookSinkConfig { } /// File Sink Configuration - Add buffering parameters -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default)] pub struct FileSinkConfig { pub enabled: bool, pub path: String, @@ -44,7 +44,7 @@ pub struct FileSinkConfig { } /// Sink configuration collection -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default)] pub struct SinkConfig { pub kafka: KafkaSinkConfig, pub webhook: WebhookSinkConfig, @@ -52,13 +52,13 @@ pub struct SinkConfig { } ///Logger Configuration -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default)] pub struct LoggerConfig { pub queue_capacity: Option, } /// Overall application configuration -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default)] pub struct AppConfig { pub observability: OtelConfig, pub sinks: SinkConfig, diff --git a/packages/obs/src/entry/audit.rs b/packages/obs/src/entry/audit.rs new file mode 100644 index 000000000..4e5099857 --- /dev/null +++ b/packages/obs/src/entry/audit.rs @@ -0,0 +1,88 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +/// ObjectVersion object version key/versionId +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ObjectVersion { + #[serde(rename = "objectName")] + pub object_name: String, + #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] + pub version_id: Option, +} + +/// API details structure +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ApiDetails { + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(rename = "bucket", skip_serializing_if = "Option::is_none")] + pub bucket: Option, + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "objects", skip_serializing_if = "Vec::is_empty", default)] + pub objects: Vec, + #[serde(rename = "status", skip_serializing_if = "Option::is_none")] + pub status: Option, + #[serde(rename = "statusCode", skip_serializing_if = "Option::is_none")] + pub status_code: Option, + #[serde(rename = "rx")] + pub input_bytes: i64, + #[serde(rename = "tx")] + pub output_bytes: i64, + #[serde(rename = "txHeaders", skip_serializing_if = "Option::is_none")] + pub header_bytes: Option, + #[serde(rename = "timeToFirstByte", skip_serializing_if = "Option::is_none")] + pub time_to_first_byte: Option, + #[serde(rename = "timeToFirstByteInNS", skip_serializing_if = "Option::is_none")] + pub time_to_first_byte_in_ns: Option, + #[serde(rename = "timeToResponse", skip_serializing_if = "Option::is_none")] + pub time_to_response: Option, + #[serde(rename = "timeToResponseInNS", skip_serializing_if = "Option::is_none")] + pub time_to_response_in_ns: Option, +} + +/// Entry - audit entry logs +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuditEntry { + pub version: String, + #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")] + pub deployment_id: Option, + pub time: DateTime, + pub event: String, + + // Class of audit message - S3, admin ops, bucket management + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub entry_type: Option, + + // Deprecated, replaced by 'Event' + pub trigger: String, + pub api: ApiDetails, + #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")] + pub remote_host: Option, + #[serde(rename = "requestID", skip_serializing_if = "Option::is_none")] + pub request_id: Option, + #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")] + pub user_agent: Option, + #[serde(rename = "requestPath", skip_serializing_if = "Option::is_none")] + pub req_path: Option, + #[serde(rename = "requestHost", skip_serializing_if = "Option::is_none")] + pub req_host: Option, + #[serde(rename = "requestClaims", skip_serializing_if = "Option::is_none")] + pub req_claims: Option>, + #[serde(rename = "requestQuery", skip_serializing_if = "Option::is_none")] + pub req_query: Option>, + #[serde(rename = "requestHeader", skip_serializing_if = "Option::is_none")] + pub req_header: Option>, + #[serde(rename = "responseHeader", skip_serializing_if = "Option::is_none")] + pub resp_header: Option>, + #[serde(rename = "tags", skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(rename = "accessKey", skip_serializing_if = "Option::is_none")] + pub access_key: Option, + #[serde(rename = "parentUser", skip_serializing_if = "Option::is_none")] + pub parent_user: Option, + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option, +} diff --git a/packages/obs/src/entry/base.rs b/packages/obs/src/entry/base.rs new file mode 100644 index 000000000..aa9d22ab7 --- /dev/null +++ b/packages/obs/src/entry/base.rs @@ -0,0 +1,114 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +/// ObjectVersion object version key/versionId +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObjectVersion { + #[serde(rename = "objectName")] + pub object_name: String, + #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] + pub version_id: Option, +} + +/// Args - defines the arguments for the API +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Args { + #[serde(rename = "bucket", skip_serializing_if = "Option::is_none")] + pub bucket: Option, + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] + pub version_id: Option, + #[serde(rename = "objects", skip_serializing_if = "Option::is_none")] + pub objects: Option>, + #[serde(rename = "metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} + +/// Trace - defines the trace +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Trace { + #[serde(rename = "message", skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(rename = "source", skip_serializing_if = "Option::is_none")] + pub source: Option>, + #[serde(rename = "variables", skip_serializing_if = "Option::is_none")] + pub variables: Option>, +} + +/// API - defines the api type and its args +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct API { + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(rename = "args", skip_serializing_if = "Option::is_none")] + pub args: Option, +} + +/// Log kind/level enum +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum LogKind { + #[serde(rename = "INFO")] + Info, + #[serde(rename = "WARNING")] + Warning, + #[serde(rename = "ERROR")] + Error, + #[serde(rename = "FATAL")] + Fatal, +} + +/// Entry - defines fields and values of each log entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Entry { + #[serde(rename = "site", skip_serializing_if = "Option::is_none")] + pub site: Option, + + #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")] + pub deployment_id: Option, + + pub level: LogKind, + + #[serde(rename = "errKind", skip_serializing_if = "Option::is_none")] + pub log_kind: Option, // Deprecated Jan 2024 + + pub time: DateTime, + + #[serde(rename = "api", skip_serializing_if = "Option::is_none")] + pub api: Option, + + #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")] + pub remote_host: Option, + + #[serde(rename = "host", skip_serializing_if = "Option::is_none")] + pub host: Option, + + #[serde(rename = "requestID", skip_serializing_if = "Option::is_none")] + pub request_id: Option, + + #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")] + pub user_agent: Option, + + #[serde(rename = "message", skip_serializing_if = "Option::is_none")] + pub message: Option, + + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub trace: Option, +} + +/// Info holds console log messages +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Info { + #[serde(flatten)] + pub entry: Entry, + + pub console_msg: String, + + #[serde(rename = "node")] + pub node_name: String, + + #[serde(skip)] + pub err: Option, +} diff --git a/packages/obs/src/entry.rs b/packages/obs/src/entry/log.rs similarity index 95% rename from packages/obs/src/entry.rs rename to packages/obs/src/entry/log.rs index a8cef168a..6960fef21 100644 --- a/packages/obs/src/entry.rs +++ b/packages/obs/src/entry/log.rs @@ -52,6 +52,7 @@ pub struct LogEntry { pub level: SerializableLevel, // Log Level pub message: String, // Log messages pub source: String, // Log source (such as module name) + pub target: Option, // Log target pub request_id: Option, // Request ID (Common Server Fields) pub user_id: Option, // User ID (Common Server Fields) pub fields: Vec<(String, String)>, // Attached fields (key value pairs) @@ -63,6 +64,7 @@ impl LogEntry { level: Level, message: String, source: String, + target: Option, request_id: Option, user_id: Option, fields: Vec<(String, String)>, @@ -72,6 +74,7 @@ impl LogEntry { level: SerializableLevel::from(level), message, source, + target, request_id, user_id, fields, diff --git a/packages/obs/src/entry/mod.rs b/packages/obs/src/entry/mod.rs new file mode 100644 index 000000000..efc697e01 --- /dev/null +++ b/packages/obs/src/entry/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod audit; +mod base; +pub(crate) mod log; diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index 469547d61..079d9abfc 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -13,20 +13,53 @@ mod worker; pub use config::load_config; pub use config::{AppConfig, OtelConfig}; -pub use entry::{LogEntry, SerializableLevel}; +pub use entry::log::{LogEntry, SerializableLevel}; pub use logger::start_logger; +pub use logger::{ + ensure_logger_initialized, get_global_logger, init_global_logger, locked_logger, log_debug, log_error, log_info, log_trace, + log_warn, log_with_context, +}; pub use logger::{LogError, Logger}; pub use sink::Sink; +use std::sync::Arc; pub use telemetry::init_telemetry; +use tokio::sync::Mutex; pub use utils::{get_local_ip, get_local_ip_with_default}; pub use worker::start_worker; -/// Log module initialization function +/// Initialize the observability module /// -/// Return to Logger and Clean Guard -pub fn init_logging(config: AppConfig) -> (Logger, telemetry::OtelGuard) { +/// # Parameters +/// - `config`: Configuration information +/// +/// # Returns +/// A tuple containing the logger and the telemetry guard +/// +/// # Example +/// ``` +/// use rustfs_obs::{AppConfig, init_obs}; +/// +/// let config = AppConfig::default(); +/// let (logger, guard) = init_obs(config); +/// ``` +pub async fn init_obs(config: AppConfig) -> (Arc>, telemetry::OtelGuard) { let guard = init_telemetry(&config.observability); let sinks = sink::create_sinks(&config); - let logger = start_logger(&config, sinks); + let logger = init_global_logger(&config, sinks).await; (logger, guard) } + +/// Get the global logger instance +/// This function returns a reference to the global logger instance. +/// +/// # Returns +/// A reference to the global logger instance +/// +/// # Example +/// ``` +/// use rustfs_obs::get_logger; +/// let logger = get_logger(); +/// ``` +pub fn get_logger() -> &'static Arc> { + get_global_logger() +} diff --git a/packages/obs/src/logger.rs b/packages/obs/src/logger.rs index f2297c9ef..a3848cc3b 100644 --- a/packages/obs/src/logger.rs +++ b/packages/obs/src/logger.rs @@ -1,8 +1,14 @@ use crate::{AppConfig, LogEntry, SerializableLevel, Sink}; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; +use tokio::sync::{Mutex, OnceCell}; +use tracing_core::Level; + +// Add the global instance at the module level +static GLOBAL_LOGGER: OnceCell>> = OnceCell::const_new(); /// Server log processor +#[derive(Debug)] pub struct Logger { sender: Sender, // Log sending channel queue_capacity: usize, @@ -15,17 +21,21 @@ impl Logger { // Get queue capacity from configuration, or use default values 10000 let queue_capacity = config.logger.queue_capacity.unwrap_or(10000); let (sender, receiver) = mpsc::channel(queue_capacity); - ( - Logger { - sender, - queue_capacity, - }, - receiver, - ) + (Logger { sender, queue_capacity }, receiver) } - // Add a method to get queue capacity - pub fn queue_capacity(&self) -> usize { + /// get the queue capacity + /// This function returns the queue capacity. + /// # Returns + /// The queue capacity + /// # Example + /// ``` + /// use rustfs_obs::Logger; + /// async fn example(logger: &Logger) { + /// let _ = logger.get_queue_capacity(); + /// } + /// ``` + pub fn get_queue_capacity(&self) -> usize { self.queue_capacity } @@ -37,20 +47,20 @@ impl Logger { tracing::Span::current() .record("log_message", &entry.message) .record("source", &entry.source); + println!("target start is {:?}", &entry.target); + let target = if let Some(target) = &entry.target { + target.clone() + } else { + "server_logs".to_string() + }; - // Record queue utilization (if a certain threshold is exceeded) - let queue_len = self.sender.capacity(); - let utilization = queue_len as f64 / self.queue_capacity as f64; - if utilization > 0.8 { - tracing::warn!("Log queue utilization high: {:.1}%", utilization * 100.0); - } - + println!("target end is {:?}", target); // Generate independent Tracing Events with full LogEntry information // Generate corresponding events according to level match entry.level { - SerializableLevel(tracing::Level::ERROR) => { + SerializableLevel(Level::ERROR) => { tracing::error!( - target: "server_logs", + target = %target.clone(), timestamp = %entry.timestamp, message = %entry.message, source = %entry.source, @@ -59,9 +69,9 @@ impl Logger { fields = ?entry.fields ); } - SerializableLevel(tracing::Level::WARN) => { + SerializableLevel(Level::WARN) => { tracing::warn!( - target: "server_logs", + target = %target.clone(), timestamp = %entry.timestamp, message = %entry.message, source = %entry.source, @@ -70,9 +80,9 @@ impl Logger { fields = ?entry.fields ); } - SerializableLevel(tracing::Level::INFO) => { + SerializableLevel(Level::INFO) => { tracing::info!( - target: "server_logs", + target = %target.clone(), timestamp = %entry.timestamp, message = %entry.message, source = %entry.source, @@ -81,9 +91,9 @@ impl Logger { fields = ?entry.fields ); } - SerializableLevel(tracing::Level::DEBUG) => { + SerializableLevel(Level::DEBUG) => { tracing::debug!( - target: "server_logs", + target = %target.clone(), timestamp = %entry.timestamp, message = %entry.message, source = %entry.source, @@ -92,9 +102,9 @@ impl Logger { fields = ?entry.fields ); } - SerializableLevel(tracing::Level::TRACE) => { + SerializableLevel(Level::TRACE) => { tracing::trace!( - target: "server_logs", + target = %target.clone(), timestamp = %entry.timestamp, message = %entry.message, source = %entry.source, @@ -111,102 +121,52 @@ impl Logger { Err(mpsc::error::TrySendError::Full(entry)) => { // Processing strategy when queue is full tracing::warn!("Log queue full, applying backpressure"); - match tokio::time::timeout( - std::time::Duration::from_millis(500), - self.sender.send(entry), - ) - .await - { + match tokio::time::timeout(std::time::Duration::from_millis(500), self.sender.send(entry)).await { Ok(Ok(_)) => Ok(()), Ok(Err(_)) => Err(LogError::SendFailed("Channel closed")), Err(_) => Err(LogError::Timeout("Queue backpressure timeout")), } } - Err(mpsc::error::TrySendError::Closed(_)) => { - Err(LogError::SendFailed("Logger channel closed")) - } + Err(mpsc::error::TrySendError::Closed(_)) => Err(LogError::SendFailed("Logger channel closed")), } } - // Add convenient methods to simplify logging - // Fix the info() method, replacing None with an empty vector instead of the Option type - pub async fn info(&self, message: &str, source: &str) -> Result<(), LogError> { - self.log(LogEntry::new( - tracing::Level::INFO, - message.to_string(), - source.to_string(), - None, - None, - Vec::new(), // 使用空向量代替 None - )) - .await - } - - /// Add warn() method - pub async fn error(&self, message: &str, source: &str) -> Result<(), LogError> { - self.log(LogEntry::new( - tracing::Level::ERROR, - message.to_string(), - source.to_string(), - None, - None, - Vec::new(), - )) - .await - } - - /// Add warn() method - pub async fn warn(&self, message: &str, source: &str) -> Result<(), LogError> { - self.log(LogEntry::new( - tracing::Level::WARN, - message.to_string(), - source.to_string(), - None, - None, - Vec::new(), - )) - .await - } - - /// Add debug() method - pub async fn debug(&self, message: &str, source: &str) -> Result<(), LogError> { - self.log(LogEntry::new( - tracing::Level::DEBUG, - message.to_string(), - source.to_string(), - None, - None, - Vec::new(), - )) - .await - } - - /// Add trace() method - pub async fn trace(&self, message: &str, source: &str) -> Result<(), LogError> { - self.log(LogEntry::new( - tracing::Level::TRACE, - message.to_string(), - source.to_string(), - None, - None, - Vec::new(), - )) - .await - } - - // Add extension methods with context information for more flexibility - pub async fn info_with_context( + /// Write log with context information + /// This function writes log messages with context information. + /// + /// # Parameters + /// - `message`: Message to be logged + /// - `source`: Source of the log + /// - `request_id`: Request ID + /// - `user_id`: User ID + /// - `fields`: Additional fields + /// + /// # Returns + /// Result indicating whether the operation was successful + /// + /// # Example + /// ``` + /// use tracing_core::Level; + /// use rustfs_obs::Logger; + /// + /// async fn example(logger: &Logger) { + /// let _ = logger.write_with_context("This is an information message", "example",Level::INFO, Some("target".to_string()),Some("req-12345".to_string()), Some("user-6789".to_string()), vec![("endpoint".to_string(), "/api/v1/data".to_string())]).await; + /// } + pub async fn write_with_context( &self, message: &str, source: &str, + level: Level, + target: Option, request_id: Option, user_id: Option, fields: Vec<(String, String)>, ) -> Result<(), LogError> { self.log(LogEntry::new( - tracing::Level::INFO, + level, message.to_string(), source.to_string(), + target, request_id, user_id, fields, @@ -214,14 +174,69 @@ impl Logger { .await } - // Add elegant closing method + /// Write log + /// This function writes log messages. + /// # Parameters + /// - `message`: Message to be logged + /// - `source`: Source of the log + /// - `level`: Log level + /// + /// # Returns + /// Result indicating whether the operation was successful + /// + /// # Example + /// ``` + /// use rustfs_obs::Logger; + /// use tracing_core::Level; + /// + /// async fn example(logger: &Logger) { + /// let _ = logger.write("This is an information message", "example", Level::INFO).await; + /// } + /// ``` + pub async fn write(&self, message: &str, source: &str, level: Level) -> Result<(), LogError> { + self.log(LogEntry::new( + level, + message.to_string(), + source.to_string(), + None, + None, + None, + Vec::new(), + )) + .await + } + + /// Shutdown the logger + /// This function shuts down the logger. + /// + /// # Returns + /// Result indicating whether the operation was successful + /// + /// # Example + /// ``` + /// use rustfs_obs::Logger; + /// + /// async fn example(logger: Logger) { + /// let _ = logger.shutdown().await; + /// } + /// ``` pub async fn shutdown(self) -> Result<(), LogError> { drop(self.sender); //Close the sending end so that the receiver knows that there is no new message Ok(()) } } -// Define custom error type +/// Log error type +/// This enum defines the error types that can occur when logging. +/// It is used to provide more detailed error information. +/// # Example +/// ``` +/// use rustfs_obs::LogError; +/// use thiserror::Error; +/// +/// LogError::SendFailed("Failed to send log"); +/// LogError::Timeout("Operation timed out"); +/// ``` #[derive(Debug, thiserror::Error)] pub enum LogError { #[error("Failed to send log: {0}")] @@ -231,8 +246,245 @@ pub enum LogError { } /// Start the log module +/// This function starts the log module. +/// It initializes the logger and starts the worker to process logs. +/// # Parameters +/// - `config`: Configuration information +/// - `sinks`: A vector of Sink instances +/// # Returns +/// The global logger instance +/// # Example +/// ``` +/// use rustfs_obs::{AppConfig, start_logger}; +/// +/// let config = AppConfig::default(); +/// let sinks = vec![]; +/// let logger = start_logger(&config, sinks); +/// ``` pub fn start_logger(config: &AppConfig, sinks: Vec>) -> Logger { let (logger, receiver) = Logger::new(config); tokio::spawn(crate::worker::start_worker(receiver, sinks)); logger } + +/// Initialize the global logger instance +/// This function initializes the global logger instance and returns a reference to it. +/// If the logger has been initialized before, it will return the existing logger instance. +/// +/// # Parameters +/// - `config`: Configuration information +/// - `sinks`: A vector of Sink instances +/// +/// # Returns +/// A reference to the global logger instance +/// +/// # Example +/// ``` +/// use rustfs_obs::{AppConfig,init_global_logger}; +/// +/// let config = AppConfig::default(); +/// let sinks = vec![]; +/// let logger = init_global_logger(&config, sinks); +/// ``` +pub async fn init_global_logger(config: &AppConfig, sinks: Vec>) -> Arc> { + let logger = Arc::new(Mutex::new(start_logger(config, sinks))); + GLOBAL_LOGGER.set(logger.clone()).expect("Logger already initialized"); + logger +} + +/// Get the global logger instance +/// +/// This function returns a reference to the global logger instance. +/// +/// # Returns +/// A reference to the global logger instance +/// +/// # Example +/// ``` +/// use rustfs_obs::get_global_logger; +/// +/// let logger = get_global_logger(); +/// ``` +pub fn get_global_logger() -> &'static Arc> { + GLOBAL_LOGGER.get().expect("Logger not initialized") +} + +/// Get the global logger instance with a lock +/// This function returns a reference to the global logger instance with a lock. +/// It is used to ensure that the logger is thread-safe. +/// +/// # Returns +/// A reference to the global logger instance with a lock +/// +/// # Example +/// ``` +/// use rustfs_obs::locked_logger; +/// +/// async fn example() { +/// let logger = locked_logger().await; +/// } +/// ``` +pub async fn locked_logger() -> tokio::sync::MutexGuard<'static, Logger> { + get_global_logger().lock().await +} + +/// Initialize with default empty logger if needed (optional) +/// This function initializes the logger with a default empty logger if needed. +/// It is used to ensure that the logger is initialized before logging. +/// +/// # Returns +/// A reference to the global logger instance +/// +/// # Example +/// ``` +/// use rustfs_obs::ensure_logger_initialized; +/// +/// let logger = ensure_logger_initialized(); +/// ``` +pub fn ensure_logger_initialized() -> &'static Arc> { + if GLOBAL_LOGGER.get().is_none() { + let config = AppConfig::default(); + let sinks = vec![]; + let logger = Arc::new(Mutex::new(start_logger(&config, sinks))); + let _ = GLOBAL_LOGGER.set(logger); + } + GLOBAL_LOGGER.get().unwrap() +} + +/// Log information +/// This function logs information messages. +/// +/// # Parameters +/// - `message`: Message to be logged +/// - `source`: Source of the log +/// +/// # Returns +/// Result indicating whether the operation was successful +/// +/// # Example +/// ``` +/// use rustfs_obs::log_info; +/// +/// async fn example() { +/// let _ = log_info("This is an information message", "example").await; +/// } +/// ``` +pub async fn log_info(message: &str, source: &str) -> Result<(), LogError> { + get_global_logger().lock().await.write(message, source, Level::INFO).await +} + +/// Log error +/// This function logs error messages. +/// # Parameters +/// - `message`: Message to be logged +/// - `source`: Source of the log +/// # Returns +/// Result indicating whether the operation was successful +/// # Example +/// ``` +/// use rustfs_obs::log_error; +/// +/// async fn example() { +/// let _ = log_error("This is an error message", "example").await; +/// } +pub async fn log_error(message: &str, source: &str) -> Result<(), LogError> { + get_global_logger().lock().await.write(message, source, Level::ERROR).await +} + +/// Log warning +/// This function logs warning messages. +/// # Parameters +/// - `message`: Message to be logged +/// - `source`: Source of the log +/// # Returns +/// Result indicating whether the operation was successful +/// +/// # Example +/// ``` +/// use rustfs_obs::log_warn; +/// +/// async fn example() { +/// let _ = log_warn("This is a warning message", "example").await; +/// } +/// ``` +pub async fn log_warn(message: &str, source: &str) -> Result<(), LogError> { + get_global_logger().lock().await.write(message, source, Level::WARN).await +} + +/// Log debug +/// This function logs debug messages. +/// # Parameters +/// - `message`: Message to be logged +/// - `source`: Source of the log +/// # Returns +/// Result indicating whether the operation was successful +/// +/// # Example +/// ``` +/// use rustfs_obs::log_debug; +/// +/// async fn example() { +/// let _ = log_debug("This is a debug message", "example").await; +/// } +/// ``` +pub async fn log_debug(message: &str, source: &str) -> Result<(), LogError> { + get_global_logger().lock().await.write(message, source, Level::DEBUG).await +} + +/// Log trace +/// This function logs trace messages. +/// # Parameters +/// - `message`: Message to be logged +/// - `source`: Source of the log +/// +/// # Returns +/// Result indicating whether the operation was successful +/// +/// # Example +/// ``` +/// use rustfs_obs::log_trace; +/// +/// async fn example() { +/// let _ = log_trace("This is a trace message", "example").await; +/// } +/// ``` +pub async fn log_trace(message: &str, source: &str) -> Result<(), LogError> { + get_global_logger().lock().await.write(message, source, Level::TRACE).await +} + +/// Log with context information +/// This function logs messages with context information. +/// # Parameters +/// - `message`: Message to be logged +/// - `source`: Source of the log +/// - `level`: Log level +/// - `target`: Log target +/// - `request_id`: Request ID +/// - `user_id`: User ID +/// - `fields`: Additional fields +/// # Returns +/// Result indicating whether the operation was successful +/// # Example +/// ``` +/// use tracing_core::Level; +/// use rustfs_obs::log_with_context; +/// +/// async fn example() { +/// let _ = log_with_context("This is an information message", "example", Level::INFO, Some("target".to_string()), Some("req-12345".to_string()), Some("user-6789".to_string()), vec![("endpoint".to_string(), "/api/v1/data".to_string())]).await; +/// } +/// ``` +pub async fn log_with_context( + message: &str, + source: &str, + level: Level, + target: Option, + request_id: Option, + user_id: Option, + fields: Vec<(String, String)>, +) -> Result<(), LogError> { + get_global_logger() + .lock() + .await + .write_with_context(message, source, level, target, request_id, user_id, fields) + .await +} diff --git a/packages/obs/src/sink.rs b/packages/obs/src/sink.rs index 37785cfba..2363a69dd 100644 --- a/packages/obs/src/sink.rs +++ b/packages/obs/src/sink.rs @@ -258,10 +258,10 @@ impl FileSink { buffer_size: usize, flush_interval_ms: u64, flush_threshold: usize, - ) -> Result { + ) -> Result { let file = OpenOptions::new().append(true).create(true).open(&path).await?; - let writer = tokio::io::BufWriter::with_capacity(buffer_size, file); + let writer = io::BufWriter::with_capacity(buffer_size, file); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -421,11 +421,10 @@ pub fn create_sinks(config: &AppConfig) -> Vec> { // Use synchronous file operations let file_result = std::fs::OpenOptions::new().append(true).create(true).open(&path); - match file_result { Ok(file) => { let buffer_size = config.sinks.file.buffer_size.unwrap_or(8192); - let writer = tokio::io::BufWriter::with_capacity(buffer_size, tokio::fs::File::from_std(file)); + let writer = io::BufWriter::with_capacity(buffer_size, tokio::fs::File::from_std(file)); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/packages/obs/src/telemetry.rs b/packages/obs/src/telemetry.rs index fb1111303..cafe5badc 100644 --- a/packages/obs/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -13,6 +13,8 @@ use opentelemetry_semantic_conventions::{ attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_NAME, SERVICE_VERSION}, SCHEMA_URL, }; +use std::io::IsTerminal; +use tracing_error::ErrorLayer; use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; @@ -155,63 +157,72 @@ pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { let meter_provider = init_meter_provider(config); let tracer = tracer_provider.tracer(config.service_name.clone()); - let logger_provider = if config.endpoint.is_empty() { - SdkLoggerProvider::builder() - .with_resource(resource(config)) - .with_simple_exporter(opentelemetry_stdout::LogExporter::default()) - .build() - } else { - let exporter = opentelemetry_otlp::LogExporter::builder() - .with_tonic() - .with_endpoint(&config.endpoint) - .build() - .unwrap(); - SdkLoggerProvider::builder() - .with_resource(resource(config)) - .with_batch_exporter(exporter) - .with_batch_exporter(opentelemetry_stdout::LogExporter::default()) - .build() + // Initialize logger provider based on configuration + let logger_provider = { + let mut builder = SdkLoggerProvider::builder().with_resource(resource(config)); + + if config.endpoint.is_empty() { + // Use stdout exporter when no endpoint is configured + builder = builder.with_simple_exporter(opentelemetry_stdout::LogExporter::default()); + } else { + // Configure OTLP exporter when endpoint is provided + let exporter = opentelemetry_otlp::LogExporter::builder() + .with_tonic() + .with_endpoint(&config.endpoint) + .build() + .unwrap(); + + builder = builder.with_batch_exporter(exporter); + + // Add stdout exporter if requested + if config.use_stdout { + builder = builder.with_batch_exporter(opentelemetry_stdout::LogExporter::default()); + } + } + + builder.build() }; - let otel_layer = layer::OpenTelemetryTracingBridge::new(&logger_provider); - // For the OpenTelemetry layer, add a tracing filter to filter events from - // OpenTelemetry and its dependent crates (opentelemetry-otlp uses crates - // like reqwest/tonic etc.) from being sent back to OTel itself, thus - // preventing infinite telemetry generation. The filter levels are set as - // follows: - // - Allow `info` level and above by default. - // - Restrict `opentelemetry`, `hyper`, `tonic`, and `reqwest` completely. - // Note: This will also drop events from crates like `tonic` etc. even when - // they are used outside the OTLP Exporter. For more details, see: - // https://github.com/open-telemetry/opentelemetry-rust/issues/761 - let filter_otel = EnvFilter::new("info") - .add_directive("hyper=off".parse().unwrap()) - .add_directive("opentelemetry=off".parse().unwrap()) - .add_directive("tonic=off".parse().unwrap()) - .add_directive("h2=off".parse().unwrap()) - .add_directive("reqwest=off".parse().unwrap()); - let otel_layer = otel_layer.with_filter(filter_otel); + // Setup OpenTelemetryTracingBridge layer + let otel_layer = { + // Filter to prevent infinite telemetry loops + // This blocks events from OpenTelemetry and its dependent libraries (tonic, reqwest, etc.) + // from being sent back to OpenTelemetry itself + let filter_otel = EnvFilter::new("info") + .add_directive("hyper=off".parse().unwrap()) + .add_directive("opentelemetry=off".parse().unwrap()) + .add_directive("tonic=off".parse().unwrap()) + .add_directive("h2=off".parse().unwrap()) + .add_directive("reqwest=off".parse().unwrap()); + + layer::OpenTelemetryTracingBridge::new(&logger_provider).with_filter(filter_otel) + }; let registry = tracing_subscriber::registry() .with(tracing_subscriber::filter::LevelFilter::INFO) .with(OpenTelemetryLayer::new(tracer)) .with(MetricsLayer::new(meter_provider.clone())) .with(otel_layer); - if config.endpoint.is_empty() { - // Create a new tracing::Fmt layer to print the logs to stdout. It has a - // default filter of `info` level and above, and `debug` and above for logs - // from OpenTelemetry crates. The filter levels can be customized as needed. - let filter_fmt = EnvFilter::new("info").add_directive("opentelemetry=debug".parse().unwrap()); - let fmt_layer = tracing_subscriber::fmt::layer() - .with_thread_names(true) - .with_filter(filter_fmt); + // Configure formatting layer + let enable_color = std::io::stdout().is_terminal(); + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(enable_color) + .with_thread_names(true) + .with_file(true) + .with_line_number(true); - registry - .with(tracing_subscriber::fmt::layer().with_ansi(true)) - .with(fmt_layer) - .init(); + // Creating a formatting layer with explicit type to avoid type mismatches + let fmt_layer = if config.endpoint.is_empty() { + // Local debug mode: add filter to show OpenTelemetry debug logs + let filter_fmt = EnvFilter::new("info").add_directive("opentelemetry=debug".parse().unwrap()); + fmt_layer.with_filter(filter_fmt) } else { - registry.with(tracing_subscriber::fmt::layer().with_ansi(false)).init(); - println!("Logs and meter,tracer enabled"); + // Production mode: use default filter settings + fmt_layer.with_filter(EnvFilter::new("info").add_directive("opentelemetry=off".parse().unwrap())) + }; + + registry.with(ErrorLayer::default()).with(fmt_layer).init(); + if !config.endpoint.is_empty() { + tracing::info!("OpenTelemetry telemetry initialized with OTLP endpoint: {}", config.endpoint); } OtelGuard { diff --git a/packages/obs/src/worker.rs b/packages/obs/src/worker.rs index a04327945..1c40f47f5 100644 --- a/packages/obs/src/worker.rs +++ b/packages/obs/src/worker.rs @@ -1,4 +1,4 @@ -use crate::{entry::LogEntry, sink::Sink}; +use crate::{entry::log::LogEntry, sink::Sink}; use std::sync::Arc; use tokio::sync::mpsc::Receiver; diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index a39e121ee..b87b16ca3 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -35,6 +35,7 @@ use hyper_util::{ }; use iam::init_iam_sys; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; +use rustfs_obs::{init_obs, load_config}; use s3s::{host::MultiDomain, service::S3ServiceBuilder}; use service::hybrid; use std::{io::IsTerminal, net::SocketAddr}; @@ -45,6 +46,7 @@ use tracing::{debug, error, info, warn}; use tracing_error::ErrorLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +#[allow(dead_code)] fn setup_tracing() { use tracing_subscriber::EnvFilter; @@ -89,8 +91,10 @@ fn main() -> Result<()> { let opt = config::Opt::parse(); //设置 trace - setup_tracing(); - + // setup_tracing(); + let config = load_config(Some("packages/obs/examples/config".to_string())); + // Initialize the logger asynchronously + let (_logger, _guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); //运行参数 run(opt) } From bcdd204fa0ff4ec7e7809fc76882b82ccb0fc01f Mon Sep 17 00:00:00 2001 From: houseme Date: Mon, 17 Mar 2025 21:37:47 +0800 Subject: [PATCH 09/27] improve log struct --- packages/obs/examples/config.toml | 2 +- packages/obs/src/entry/audit.rs | 382 +++++++++++++++++++++++++++-- packages/obs/src/entry/base.rs | 390 +++++++++++++++++++++++++++++- packages/obs/src/entry/mod.rs | 2 +- packages/obs/src/lib.rs | 2 + 5 files changed, 756 insertions(+), 22 deletions(-) diff --git a/packages/obs/examples/config.toml b/packages/obs/examples/config.toml index 2ceac4397..ddf376352 100644 --- a/packages/obs/examples/config.toml +++ b/packages/obs/examples/config.toml @@ -1,6 +1,6 @@ [observability] endpoint = "http://localhost:4317" -use_stdout = true +use_stdout = false sample_ratio = 0.5 meter_interval = 30 service_name = "rustfs_obs_service" diff --git a/packages/obs/src/entry/audit.rs b/packages/obs/src/entry/audit.rs index 4e5099857..8f8b1e34f 100644 --- a/packages/obs/src/entry/audit.rs +++ b/packages/obs/src/entry/audit.rs @@ -1,19 +1,11 @@ +use crate::ObjectVersion; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -/// ObjectVersion object version key/versionId -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ObjectVersion { - #[serde(rename = "objectName")] - pub object_name: String, - #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] - pub version_id: Option, -} - /// API details structure -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct ApiDetails { #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, @@ -43,8 +35,107 @@ pub struct ApiDetails { pub time_to_response_in_ns: Option, } +impl ApiDetails { + /// Create a new `ApiDetails` with default values + pub fn new() -> Self { + ApiDetails { + name: None, + bucket: None, + object: None, + objects: Vec::new(), + status: None, + status_code: None, + input_bytes: 0, + output_bytes: 0, + header_bytes: None, + time_to_first_byte: None, + time_to_first_byte_in_ns: None, + time_to_response: None, + time_to_response_in_ns: None, + } + } + + /// Set the name + pub fn set_name(mut self, name: Option) -> Self { + self.name = name; + self + } + + /// Set the bucket + pub fn set_bucket(mut self, bucket: Option) -> Self { + self.bucket = bucket; + self + } + + /// Set the object + pub fn set_object(mut self, object: Option) -> Self { + self.object = object; + self + } + + /// Set the objects + pub fn set_objects(mut self, objects: Vec) -> Self { + self.objects = objects; + self + } + + /// Set the status + pub fn set_status(mut self, status: Option) -> Self { + self.status = status; + self + } + + /// Set the status code + pub fn set_status_code(mut self, status_code: Option) -> Self { + self.status_code = status_code; + self + } + + /// Set the input bytes + pub fn set_input_bytes(mut self, input_bytes: i64) -> Self { + self.input_bytes = input_bytes; + self + } + + /// Set the output bytes + pub fn set_output_bytes(mut self, output_bytes: i64) -> Self { + self.output_bytes = output_bytes; + self + } + + /// Set the header bytes + pub fn set_header_bytes(mut self, header_bytes: Option) -> Self { + self.header_bytes = header_bytes; + self + } + + /// Set the time to first byte + pub fn set_time_to_first_byte(mut self, time_to_first_byte: Option) -> Self { + self.time_to_first_byte = time_to_first_byte; + self + } + + /// Set the time to first byte in nanoseconds + pub fn set_time_to_first_byte_in_ns(mut self, time_to_first_byte_in_ns: Option) -> Self { + self.time_to_first_byte_in_ns = time_to_first_byte_in_ns; + self + } + + /// Set the time to response + pub fn set_time_to_response(mut self, time_to_response: Option) -> Self { + self.time_to_response = time_to_response; + self + } + + /// Set the time to response in nanoseconds + pub fn set_time_to_response_in_ns(mut self, time_to_response_in_ns: Option) -> Self { + self.time_to_response_in_ns = time_to_response_in_ns; + self + } +} + /// Entry - audit entry logs -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct AuditEntry { pub version: String, #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")] @@ -55,9 +146,6 @@ pub struct AuditEntry { // Class of audit message - S3, admin ops, bucket management #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub entry_type: Option, - - // Deprecated, replaced by 'Event' - pub trigger: String, pub api: ApiDetails, #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")] pub remote_host: Option, @@ -86,3 +174,269 @@ pub struct AuditEntry { #[serde(rename = "error", skip_serializing_if = "Option::is_none")] pub error: Option, } + +impl AuditEntry { + /// Create a new `AuditEntry` with default values + pub fn new() -> Self { + AuditEntry { + version: String::new(), + deployment_id: None, + time: Utc::now(), + event: String::new(), + entry_type: None, + api: ApiDetails::new(), + remote_host: None, + request_id: None, + user_agent: None, + req_path: None, + req_host: None, + req_claims: None, + req_query: None, + req_header: None, + resp_header: None, + tags: None, + access_key: None, + parent_user: None, + error: None, + } + } + + /// Create a new `AuditEntry` with version and time event and api details + /// # Arguments + /// * `version` - Version of the audit entry + /// * `time` - Time of the audit entry + /// * `event` - Event of the audit entry + /// * `api` - API details of the audit entry + /// # Returns + /// * `AuditEntry` with the given values + /// # Example + /// ``` + /// use chrono::Utc; + /// use rustfs_obs::{ApiDetails, AuditEntry}; + /// let entry = AuditEntry::new_with_values( + /// "v1".to_string(), + /// Utc::now(), + /// "event".to_string(), + /// ApiDetails::new(), + /// ); + /// ``` + /// # Remarks + /// This is a convenience method to create an `AuditEntry` with the given values + /// without having to set each field individually + /// This is useful when you want to create an `AuditEntry` with the given values + /// without having to set each field individually + pub fn new_with_values(version: String, time: DateTime, event: String, api: ApiDetails) -> Self { + AuditEntry { + version, + deployment_id: None, + time, + event, + entry_type: None, + api, + remote_host: None, + request_id: None, + user_agent: None, + req_path: None, + req_host: None, + req_claims: None, + req_query: None, + req_header: None, + resp_header: None, + tags: None, + access_key: None, + parent_user: None, + error: None, + } + } + + /// Set the version + pub fn set_version(mut self, version: String) -> Self { + self.version = version; + self + } + + /// Set the deployment ID + pub fn set_deployment_id(mut self, deployment_id: Option) -> Self { + self.deployment_id = deployment_id; + self + } + + /// Set the time + pub fn set_time(mut self, time: DateTime) -> Self { + self.time = time; + self + } + + /// Set the event + pub fn set_event(mut self, event: String) -> Self { + self.event = event; + self + } + + /// Set the entry type + pub fn set_entry_type(mut self, entry_type: Option) -> Self { + self.entry_type = entry_type; + self + } + + /// Set the API details + pub fn set_api(mut self, api: ApiDetails) -> Self { + self.api = api; + self + } + + /// Set the remote host + pub fn set_remote_host(mut self, remote_host: Option) -> Self { + self.remote_host = remote_host; + self + } + + /// Set the request ID + pub fn set_request_id(mut self, request_id: Option) -> Self { + self.request_id = request_id; + self + } + + /// Set the user agent + pub fn set_user_agent(mut self, user_agent: Option) -> Self { + self.user_agent = user_agent; + self + } + + /// Set the request path + pub fn set_req_path(mut self, req_path: Option) -> Self { + self.req_path = req_path; + self + } + + /// Set the request host + pub fn set_req_host(mut self, req_host: Option) -> Self { + self.req_host = req_host; + self + } + + /// Set the request claims + pub fn set_req_claims(mut self, req_claims: Option>) -> Self { + self.req_claims = req_claims; + self + } + + /// Set the request query + pub fn set_req_query(mut self, req_query: Option>) -> Self { + self.req_query = req_query; + self + } + + /// Set the request header + pub fn set_req_header(mut self, req_header: Option>) -> Self { + self.req_header = req_header; + self + } + + /// Set the response header + pub fn set_resp_header(mut self, resp_header: Option>) -> Self { + self.resp_header = resp_header; + self + } + + /// Set the tags + pub fn set_tags(mut self, tags: Option>) -> Self { + self.tags = tags; + self + } + + /// Set the access key + pub fn set_access_key(mut self, access_key: Option) -> Self { + self.access_key = access_key; + self + } + + /// Set the parent user + pub fn set_parent_user(mut self, parent_user: Option) -> Self { + self.parent_user = parent_user; + self + } + + /// Set the error + pub fn set_error(mut self, error: Option) -> Self { + self.error = error; + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_audit_entry() { + let entry = AuditEntry::new() + .set_version("v1".to_string()) + .set_deployment_id(Some("12345".to_string())) + .set_time(Utc::now()) + .set_event("event".to_string()) + .set_entry_type(Some("type".to_string())) + .set_api(ApiDetails::new()) + .set_remote_host(Some("localhost".to_string())) + .set_request_id(Some("req-12345".to_string())) + .set_user_agent(Some("user-agent".to_string())) + .set_req_path(Some("/path".to_string())) + .set_req_host(Some("localhost".to_string())) + .set_req_claims(Some(HashMap::new())) + .set_req_query(Some(HashMap::new())) + .set_req_header(Some(HashMap::new())) + .set_resp_header(Some(HashMap::new())) + .set_tags(Some(HashMap::new())) + .set_access_key(Some("access-key".to_string())) + .set_parent_user(Some("parent-user".to_string())) + .set_error(Some("error".to_string())); + + assert_eq!(entry.version, "v1"); + assert_eq!(entry.deployment_id, Some("12345".to_string())); + assert_eq!(entry.event, "event"); + assert_eq!(entry.entry_type, Some("type".to_string())); + assert_eq!(entry.remote_host, Some("localhost".to_string())); + assert_eq!(entry.request_id, Some("req-12345".to_string())); + assert_eq!(entry.user_agent, Some("user-agent".to_string())); + assert_eq!(entry.req_path, Some("/path".to_string())); + assert_eq!(entry.req_host, Some("localhost".to_string())); + assert_eq!(entry.access_key, Some("access-key".to_string())); + assert_eq!(entry.parent_user, Some("parent-user".to_string())); + assert_eq!(entry.error, Some("error".to_string())); + } + + #[test] + fn test_api_details() { + let api = ApiDetails::new() + .set_name(Some("name".to_string())) + .set_bucket(Some("bucket".to_string())) + .set_object(Some("object".to_string())) + .set_objects(vec![ObjectVersion { + object_name: "object".to_string(), + version_id: Some("12345".to_string()), + }]) + .set_status(Some("status".to_string())) + .set_status_code(Some(200)) + .set_input_bytes(100) + .set_output_bytes(200) + .set_header_bytes(Some(300)) + .set_time_to_first_byte(Some("100ms".to_string())) + .set_time_to_first_byte_in_ns(Some("100ns".to_string())) + .set_time_to_response(Some("200ms".to_string())) + .set_time_to_response_in_ns(Some("200ns".to_string())); + + assert_eq!(api.name, Some("name".to_string())); + assert_eq!(api.bucket, Some("bucket".to_string())); + assert_eq!(api.object, Some("object".to_string())); + assert_eq!(api.objects.len(), 1); + assert_eq!(api.status, Some("status".to_string())); + assert_eq!(api.status_code, Some(200)); + assert_eq!(api.input_bytes, 100); + assert_eq!(api.output_bytes, 200); + assert_eq!(api.header_bytes, Some(300)); + assert_eq!(api.time_to_first_byte, Some("100ms".to_string())); + assert_eq!(api.time_to_first_byte_in_ns, Some("100ns".to_string())); + assert_eq!(api.time_to_response, Some("200ms".to_string())); + assert_eq!(api.time_to_response_in_ns, Some("200ns".to_string())); + } +} diff --git a/packages/obs/src/entry/base.rs b/packages/obs/src/entry/base.rs index aa9d22ab7..c629c74d8 100644 --- a/packages/obs/src/entry/base.rs +++ b/packages/obs/src/entry/base.rs @@ -4,7 +4,7 @@ use serde_json::Value; use std::collections::HashMap; /// ObjectVersion object version key/versionId -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)] pub struct ObjectVersion { #[serde(rename = "objectName")] pub object_name: String, @@ -12,8 +12,37 @@ pub struct ObjectVersion { pub version_id: Option, } +impl ObjectVersion { + /// Create a new ObjectVersion object + pub fn new() -> Self { + ObjectVersion { + object_name: String::new(), + version_id: None, + } + } + + /// Create a new ObjectVersion object with object name + pub fn new_with_object_name(object_name: String) -> Self { + ObjectVersion { + object_name, + version_id: None, + } + } + /// Set the object name + pub fn set_object_name(mut self, object_name: String) -> Self { + self.object_name = object_name; + self + } + + /// Set the version ID + pub fn set_version_id(mut self, version_id: Option) -> Self { + self.version_id = version_id; + self + } +} + /// Args - defines the arguments for the API -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct Args { #[serde(rename = "bucket", skip_serializing_if = "Option::is_none")] pub bucket: Option, @@ -27,8 +56,51 @@ pub struct Args { pub metadata: Option>, } +impl Args { + /// Create a new Args object + pub fn new() -> Self { + Args { + bucket: None, + object: None, + version_id: None, + objects: None, + metadata: None, + } + } + + /// Set the bucket + pub fn set_bucket(mut self, bucket: Option) -> Self { + self.bucket = bucket; + self + } + + /// Set the object + pub fn set_object(mut self, object: Option) -> Self { + self.object = object; + self + } + + /// Set the version ID + pub fn set_version_id(mut self, version_id: Option) -> Self { + self.version_id = version_id; + self + } + + /// Set the objects + pub fn set_objects(mut self, objects: Option>) -> Self { + self.objects = objects; + self + } + + /// Set the metadata + pub fn set_metadata(mut self, metadata: Option>) -> Self { + self.metadata = metadata; + self + } +} + /// Trace - defines the trace -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct Trace { #[serde(rename = "message", skip_serializing_if = "Option::is_none")] pub message: Option, @@ -38,8 +110,37 @@ pub struct Trace { pub variables: Option>, } +impl Trace { + /// Create a new Trace object + pub fn new() -> Self { + Trace { + message: None, + source: None, + variables: None, + } + } + + /// Set the message + pub fn set_message(mut self, message: Option) -> Self { + self.message = message; + self + } + + /// Set the source + pub fn set_source(mut self, source: Option>) -> Self { + self.source = source; + self + } + + /// Set the variables + pub fn set_variables(mut self, variables: Option>) -> Self { + self.variables = variables; + self + } +} + /// API - defines the api type and its args -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct API { #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, @@ -47,8 +148,27 @@ pub struct API { pub args: Option, } +impl API { + /// Create a new API object + pub fn new() -> Self { + API { name: None, args: None } + } + + /// Set the name + pub fn set_name(mut self, name: Option) -> Self { + self.name = name; + self + } + + /// Set the args + pub fn set_args(mut self, args: Option) -> Self { + self.args = args; + self + } +} + /// Log kind/level enum -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum LogKind { #[serde(rename = "INFO")] Info, @@ -60,8 +180,14 @@ pub enum LogKind { Fatal, } +impl Default for LogKind { + fn default() -> Self { + LogKind::Info + } +} + /// Entry - defines fields and values of each log entry -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct Entry { #[serde(rename = "site", skip_serializing_if = "Option::is_none")] pub site: Option, @@ -98,6 +224,98 @@ pub struct Entry { pub trace: Option, } +impl Entry { + /// Create a new Entry object with default values + pub fn new() -> Self { + Entry { + site: None, + deployment_id: None, + level: LogKind::Info, + log_kind: None, + time: Utc::now(), + api: None, + remote_host: None, + host: None, + request_id: None, + user_agent: None, + message: None, + trace: None, + } + } + + /// Set the site + pub fn set_site(mut self, site: Option) -> Self { + self.site = site; + self + } + + /// Set the deployment ID + pub fn set_deployment_id(mut self, deployment_id: Option) -> Self { + self.deployment_id = deployment_id; + self + } + + /// Set the level + pub fn set_level(mut self, level: LogKind) -> Self { + self.level = level; + self + } + + /// Set the log kind + pub fn set_log_kind(mut self, log_kind: Option) -> Self { + self.log_kind = log_kind; + self + } + + /// Set the time + pub fn set_time(mut self, time: DateTime) -> Self { + self.time = time; + self + } + + /// Set the API + pub fn set_api(mut self, api: Option) -> Self { + self.api = api; + self + } + + /// Set the remote host + pub fn set_remote_host(mut self, remote_host: Option) -> Self { + self.remote_host = remote_host; + self + } + + /// Set the host + pub fn set_host(mut self, host: Option) -> Self { + self.host = host; + self + } + + /// Set the request ID + pub fn set_request_id(mut self, request_id: Option) -> Self { + self.request_id = request_id; + self + } + + /// Set the user agent + pub fn set_user_agent(mut self, user_agent: Option) -> Self { + self.user_agent = user_agent; + self + } + + /// Set the message + pub fn set_message(mut self, message: Option) -> Self { + self.message = message; + self + } + + /// Set the trace + pub fn set_trace(mut self, trace: Option) -> Self { + self.trace = trace; + self + } +} + /// Info holds console log messages #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Info { @@ -112,3 +330,163 @@ pub struct Info { #[serde(skip)] pub err: Option, } + +impl Info { + /// Create a new Info object with default values + pub fn new() -> Self { + Info { + entry: Entry::new(), + console_msg: String::new(), + node_name: String::new(), + err: None, + } + } + + /// Create a new Info object with console message and node name + pub fn new_with_console_msg(console_msg: String, node_name: String) -> Self { + Info { + entry: Entry::new(), + console_msg, + node_name, + err: None, + } + } + + /// Set the node name + pub fn set_node_name(&mut self, node_name: String) { + self.node_name = node_name; + } + + /// Set the entry + pub fn set_entry(&mut self, entry: Entry) { + self.entry = entry; + } + + /// Set the console message + pub fn set_console_msg(&mut self, console_msg: String) { + self.console_msg = console_msg; + } + + /// Set the error message + pub fn set_err(&mut self, err: Option) { + self.err = err; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_object_version() { + let mut object_version = ObjectVersion::new(); + object_version = object_version.clone().set_object_name("object".to_string()); + object_version = object_version.clone().set_version_id(Some("version".to_string())); + assert_eq!(object_version.object_name, "object".to_string()); + assert_eq!(object_version.version_id, Some("version".to_string())); + } + + #[test] + fn test_object_version_with_object_name() { + let object_version = ObjectVersion::new_with_object_name("object".to_string()); + assert_eq!(object_version.object_name, "object".to_string()); + assert_eq!(object_version.version_id, None); + } + + #[test] + fn test_args() { + let mut obj = ObjectVersion::new(); + obj.object_name = "object".to_string(); + obj.version_id = Some("version".to_string()); + let objs = vec![obj]; + let args = Args::new() + .set_bucket(Some("bucket".to_string())) + .set_object(Some("object".to_string())) + .set_version_id(Some("version".to_string())) + .set_objects(Some(objs.clone())) + .set_metadata(Some(HashMap::new())); + + assert_eq!(args.bucket, Some("bucket".to_string())); + assert_eq!(args.object, Some("object".to_string())); + assert_eq!(args.version_id, Some("version".to_string())); + assert_eq!(args.objects, Some(objs)); + assert_eq!(args.metadata, Some(HashMap::new())); + } + + #[test] + fn test_trace() { + let trace = Trace::new() + .set_message(Some("message".to_string())) + .set_source(Some(vec!["source".to_string()])) + .set_variables(Some(HashMap::new())); + + assert_eq!(trace.message, Some("message".to_string())); + assert_eq!(trace.source, Some(vec!["source".to_string()])); + assert_eq!(trace.variables, Some(HashMap::new())); + } + + #[test] + fn test_api() { + let api = API::new().set_name(Some("name".to_string())).set_args(Some(Args::new())); + + assert_eq!(api.name, Some("name".to_string())); + assert_eq!(api.args, Some(Args::new())); + } + + #[test] + fn test_log_kind() { + assert_eq!(LogKind::default(), LogKind::Info); + } + + #[test] + fn test_entry() { + let entry = Entry::new() + .set_site(Some("site".to_string())) + .set_deployment_id(Some("deployment_id".to_string())) + .set_level(LogKind::Info) + .set_log_kind(Some(LogKind::Info)) + .set_time(Utc::now()) + .set_api(Some(API::new())) + .set_remote_host(Some("remote_host".to_string())) + .set_host(Some("host".to_string())) + .set_request_id(Some("request_id".to_string())) + .set_user_agent(Some("user_agent".to_string())) + .set_message(Some("message".to_string())) + .set_trace(Some(Trace::new())); + + assert_eq!(entry.site, Some("site".to_string())); + assert_eq!(entry.deployment_id, Some("deployment_id".to_string())); + assert_eq!(entry.level, LogKind::Info); + assert_eq!(entry.log_kind, Some(LogKind::Info)); + assert_eq!(entry.api, Some(API::new())); + assert_eq!(entry.remote_host, Some("remote_host".to_string())); + assert_eq!(entry.host, Some("host".to_string())); + assert_eq!(entry.request_id, Some("request_id".to_string())); + assert_eq!(entry.user_agent, Some("user_agent".to_string())); + assert_eq!(entry.message, Some("message".to_string())); + assert_eq!(entry.trace, Some(Trace::new())); + } + + #[test] + fn test_info() { + let mut info = Info::new(); + info.set_node_name("node_name".to_string()); + info.set_entry(Entry::new()); + info.set_console_msg("console_msg".to_string()); + info.set_err(Some("err".to_string())); + + assert_eq!(info.node_name, "node_name".to_string()); + // assert_eq!(info.entry, Entry::new()); + assert_eq!(info.console_msg, "console_msg".to_string()); + assert_eq!(info.err, Some("err".to_string())); + } + + #[test] + fn test_info_with_console_msg() { + let info = Info::new_with_console_msg("console_msg".to_string(), "node_name".to_string()); + + assert_eq!(info.node_name, "node_name".to_string()); + assert_eq!(info.console_msg, "console_msg".to_string()); + assert_eq!(info.err, None); + } +} diff --git a/packages/obs/src/entry/mod.rs b/packages/obs/src/entry/mod.rs index efc697e01..51ba15848 100644 --- a/packages/obs/src/entry/mod.rs +++ b/packages/obs/src/entry/mod.rs @@ -1,3 +1,3 @@ pub(crate) mod audit; -mod base; +pub(crate) mod base; pub(crate) mod log; diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index 079d9abfc..8d00c7b09 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -13,6 +13,8 @@ mod worker; pub use config::load_config; pub use config::{AppConfig, OtelConfig}; +pub use entry::audit::{ApiDetails, AuditEntry}; +pub use entry::base::{Args, Entry, Info, LogKind, ObjectVersion, Trace, API}; pub use entry::log::{LogEntry, SerializableLevel}; pub use logger::start_logger; pub use logger::{ From c3ecfeae6cd8a6ea2a3998122e30df513e7523f3 Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 18 Mar 2025 16:36:23 +0800 Subject: [PATCH 10/27] improve logger entry for Observability --- .docker/observability/README.md | 0 .docker/observability/docker-compose.yml | 65 +++ .docker/observability/jaeger-config.yaml | 88 +++ .docker/observability/loki-config.yaml | 63 +++ .../observability/otel-collector-config.yaml | 57 ++ .docker/observability/prometheus.yml | 11 + config/obs.example.toml | 31 ++ packages/obs/examples/server.rs | 30 +- packages/obs/src/config.rs | 65 ++- packages/obs/src/entry/args.rs | 74 +++ packages/obs/src/entry/audit.rs | 275 ++++----- packages/obs/src/entry/base.rs | 526 +++--------------- packages/obs/src/entry/log.rs | 83 --- packages/obs/src/entry/mod.rs | 142 ++++- packages/obs/src/entry/unified.rs | 279 ++++++++++ packages/obs/src/lib.rs | 32 +- packages/obs/src/logger.rs | 177 +++--- packages/obs/src/sink.rs | 27 +- packages/obs/src/worker.rs | 4 +- 19 files changed, 1208 insertions(+), 821 deletions(-) create mode 100644 .docker/observability/README.md create mode 100644 .docker/observability/docker-compose.yml create mode 100644 .docker/observability/jaeger-config.yaml create mode 100644 .docker/observability/loki-config.yaml create mode 100644 .docker/observability/otel-collector-config.yaml create mode 100644 .docker/observability/prometheus.yml create mode 100644 config/obs.example.toml create mode 100644 packages/obs/src/entry/args.rs delete mode 100644 packages/obs/src/entry/log.rs create mode 100644 packages/obs/src/entry/unified.rs diff --git a/.docker/observability/README.md b/.docker/observability/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/.docker/observability/docker-compose.yml b/.docker/observability/docker-compose.yml new file mode 100644 index 000000000..d6bd19323 --- /dev/null +++ b/.docker/observability/docker-compose.yml @@ -0,0 +1,65 @@ +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:0.120.0 + environment: + - TZ=Asia/Shanghai + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml + ports: + - 1888:1888 + - 8888:8888 + - 8889:8889 + - 13133:13133 + - 4317:4317 + - 4318:4318 + - 55679:55679 + networks: + - otel-network + jaeger: + image: jaegertracing/jaeger:2.3.0 + environment: + - TZ=Asia/Shanghai + ports: + - "16686:16686" + - "14317:4317" + - "14318:4318" + networks: + - otel-network + prometheus: + image: prom/prometheus:v3.2.0 + environment: + - TZ=Asia/Shanghai + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + networks: + - otel-network + loki: + image: grafana/loki:3.4.2 + environment: + - TZ=Asia/Shanghai + volumes: + - ./loki-config.yaml:/etc/loki/local-config.yaml + ports: + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml + networks: + - otel-network + grafana: + image: grafana/grafana:11.5.2 + ports: + - "3000:3000" # Web UI + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - TZ=Asia/Shanghai + networks: + - otel-network + + +networks: + otel-network: + driver: bridge + name: "network_otel_config" + driver_opts: + com.docker.network.enable_ipv6: "true" diff --git a/.docker/observability/jaeger-config.yaml b/.docker/observability/jaeger-config.yaml new file mode 100644 index 000000000..31b5a9d6d --- /dev/null +++ b/.docker/observability/jaeger-config.yaml @@ -0,0 +1,88 @@ + +service: + extensions: [jaeger_storage, jaeger_query, remote_sampling, healthcheckv2] + pipelines: + traces: + receivers: [otlp, jaeger, zipkin] + processors: [batch, adaptive_sampling] + exporters: [jaeger_storage_exporter] + telemetry: + resource: + service.name: jaeger + metrics: + level: detailed + readers: + - pull: + exporter: + prometheus: + host: 0.0.0.0 + port: 8888 + logs: + level: debug + # TODO Initialize telemetry tracer once OTEL released new feature. + # https://github.com/open-telemetry/opentelemetry-collector/issues/10663 + +extensions: + healthcheckv2: + use_v2: true + http: + + # pprof: + # endpoint: 0.0.0.0:1777 + # zpages: + # endpoint: 0.0.0.0:55679 + + jaeger_query: + storage: + traces: some_store + traces_archive: another_store + ui: + config_file: ./cmd/jaeger/config-ui.json + # The maximum duration that is considered for clock skew adjustments. + # Defaults to 0 seconds, which means it's disabled. + max_clock_skew_adjust: 0s + + jaeger_storage: + backends: + some_store: + memory: + max_traces: 100000 + another_store: + memory: + max_traces: 100000 + + remote_sampling: + # You can either use file or adaptive sampling strategy in remote_sampling + # file: + # path: ./cmd/jaeger/sampling-strategies.json + adaptive: + sampling_store: some_store + initial_sampling_probability: 0.1 + http: + grpc: + +receivers: + otlp: + protocols: + grpc: + http: + + jaeger: + protocols: + grpc: + thrift_binary: + thrift_compact: + thrift_http: + + zipkin: + +processors: + batch: + # Adaptive Sampling Processor is required to support adaptive sampling. + # It expects remote_sampling extension with `adaptive:` config to be enabled. + adaptive_sampling: + +exporters: + jaeger_storage_exporter: + trace_storage: some_store + diff --git a/.docker/observability/loki-config.yaml b/.docker/observability/loki-config.yaml new file mode 100644 index 000000000..4aff8772a --- /dev/null +++ b/.docker/observability/loki-config.yaml @@ -0,0 +1,63 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + log_level: debug + grpc_server_max_concurrent_streams: 1000 + +common: + instance_addr: 127.0.0.1 + path_prefix: /tmp/loki + storage: + filesystem: + chunks_directory: /tmp/loki/chunks + rules_directory: /tmp/loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +limits_config: + metric_aggregation_enabled: true + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +pattern_ingester: + enabled: true + metric_aggregation: + loki_address: localhost:3100 + +ruler: + alertmanager_url: http://localhost:9093 + +frontend: + encoding: protobuf + +# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration +# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/ +# +# Statistics help us better understand how Loki is used, and they show us performance +# levels for most users. This helps us prioritize features and documentation. +# For more information on what's sent, look at +# https://github.com/grafana/loki/blob/main/pkg/analytics/stats.go +# Refer to the buildReport method to see what goes into a report. +# +# If you would like to disable reporting, uncomment the following lines: +#analytics: +# reporting_enabled: false diff --git a/.docker/observability/otel-collector-config.yaml b/.docker/observability/otel-collector-config.yaml new file mode 100644 index 000000000..9e27c4e22 --- /dev/null +++ b/.docker/observability/otel-collector-config.yaml @@ -0,0 +1,57 @@ +receivers: + otlp: + protocols: + grpc: # OTLP gRPC 接收器 + endpoint: 0.0.0.0:4317 + http: # OTLP HTTP 接收器 + endpoint: 0.0.0.0:4318 + +processors: + batch: # 批处理处理器,提升吞吐量 + timeout: 5s + send_batch_size: 1000 + memory_limiter: + check_interval: 1s + limit_mib: 512 + +exporters: + otlp/traces: # OTLP 导出器,用于跟踪数据 + endpoint: "jaeger:4317" # Jaeger 的 OTLP gRPC 端点 + tls: + insecure: true # 开发环境禁用 TLS,生产环境需配置证书 + prometheus: # Prometheus 导出器,用于指标数据 + endpoint: "0.0.0.0:8889" # Prometheus 刮取端点 + namespace: "otel" # 指标前缀 + send_timestamps: true # 发送时间戳 + # enable_open_metrics: true + loki: # Loki 导出器,用于日志数据 + # endpoint: "http://loki:3100/otlp/v1/logs" + endpoint: "http://loki:3100/loki/api/v1/push" + tls: + insecure: true +extensions: + health_check: + pprof: + zpages: +service: + extensions: [health_check, pprof, zpages] # 启用扩展 + pipelines: + traces: + receivers: [otlp] + processors: [memory_limiter,batch] + exporters: [otlp/traces] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] + logs: + receivers: [otlp] + processors: [batch] + exporters: [loki] + telemetry: + logs: + level: "info" # Collector 日志级别 + metrics: + address: "0.0.0.0:8888" # Collector 自身指标暴露 + + diff --git a/.docker/observability/prometheus.yml b/.docker/observability/prometheus.yml new file mode 100644 index 000000000..8d3a031ef --- /dev/null +++ b/.docker/observability/prometheus.yml @@ -0,0 +1,11 @@ +global: + scrape_interval: 5s # 刮取间隔 + +scrape_configs: + - job_name: 'otel-collector' + static_configs: + - targets: ['otel-collector:8888'] # 从 Collector 刮取指标 + - job_name: 'otel-metrics' + static_configs: + - targets: ['otel-collector:8889'] # 应用指标 + diff --git a/config/obs.example.toml b/config/obs.example.toml new file mode 100644 index 000000000..59bd0f344 --- /dev/null +++ b/config/obs.example.toml @@ -0,0 +1,31 @@ +[observability] +endpoint = "" # Default is "http://localhost:4317" if not specified +use_stdout = false +sample_ratio = 0.5 +meter_interval = 30 +service_name = "rustfs_obs_service" +service_version = "0.1.0" +deployment_environment = "develop" + +[sinks] +[sinks.kafka] # Kafka sink is disabled by default +enabled = false +bootstrap_servers = "localhost:9092" +topic = "logs" +batch_size = 100 # Default is 100 if not specified +batch_timeout_ms = 1000 # Default is 1000ms if not specified + +[sinks.webhook] +enabled = false +url = "http://localhost:8080/webhook" +batch_size = 100 # Default is 3 if not specified +batch_timeout_ms = 1000 # Default is 100ms if not specified + +[sinks.file] +enabled = true +path = "logs/app.log" +batch_size = 100 +batch_timeout_ms = 1000 # Default is 8192 bytes if not specified + +[logger] +queue_capacity = 10000 \ No newline at end of file diff --git a/packages/obs/examples/server.rs b/packages/obs/examples/server.rs index 60b275de2..2d35c8d4f 100644 --- a/packages/obs/examples/server.rs +++ b/packages/obs/examples/server.rs @@ -1,12 +1,13 @@ use opentelemetry::global; -use rustfs_obs::{get_logger, init_obs, load_config, log_info, LogEntry}; +use rustfs_obs::{get_logger, init_obs, load_config, log_info, ServerLogEntry}; use std::time::{Duration, SystemTime}; use tracing::{info, instrument}; use tracing_core::Level; #[tokio::main] async fn main() { - let config = load_config(Some("packages/obs/examples/config".to_string())); + let obs_conf = Some("packages/obs/examples/config.toml".to_string()); + let config = load_config(obs_conf); let (_logger, _guard) = init_obs(config.clone()).await; // Simulate the operation tokio::time::sleep(Duration::from_millis(100)).await; @@ -33,24 +34,13 @@ async fn run(bucket: String, object: String, user: String, service_name: String) &[opentelemetry::KeyValue::new("operation", "run")], ); - let result = get_logger() - .lock() - .await - .log(LogEntry::new( - Level::INFO, - "Process user requests".to_string(), - "api_handler".to_string(), - Some("demo-audit".to_string()), - Some("req-12345".to_string()), - Some(user), - vec![ - ("endpoint".to_string(), "/api/v1/data".to_string()), - ("method".to_string(), "GET".to_string()), - ("bucket".to_string(), bucket), - ("object-length".to_string(), object.len().to_string()), - ], - )) - .await; + let server_entry = ServerLogEntry::new(Level::INFO, "api_handler".to_string()) + .user_id(Some(user.clone())) + .add_field("operation".to_string(), "login".to_string()) + .add_field("bucket".to_string(), bucket.clone()) + .add_field("object".to_string(), object.clone()); + + let result = get_logger().lock().await.log_server_entry(server_entry).await; info!("Logging is completed {:?}", result); put_object("bucket".to_string(), "object".to_string(), "user".to_string()).await; info!("Logging is completed"); diff --git a/packages/obs/src/config.rs b/packages/obs/src/config.rs index c6e57c421..744c0ec16 100644 --- a/packages/obs/src/config.rs +++ b/packages/obs/src/config.rs @@ -3,6 +3,11 @@ use serde::Deserialize; use std::env; /// OpenTelemetry Configuration +/// Add service name, service version, deployment environment +/// Add interval time for metric collection +/// Add sample ratio for trace sampling +/// Add endpoint for metric collection +/// Add use_stdout for output to stdout #[derive(Debug, Deserialize, Clone, Default)] pub struct OtelConfig { pub endpoint: String, @@ -58,6 +63,19 @@ pub struct LoggerConfig { } /// Overall application configuration +/// Add observability, sinks, and logger configuration +/// +/// Observability: OpenTelemetry configuration +/// Sinks: Kafka, Webhook, File sink configuration +/// Logger: Logger configuration +/// +/// # Example +/// ``` +/// use rustfs_obs::AppConfig; +/// use rustfs_obs::load_config; +/// +/// let config = load_config(None); +/// ``` #[derive(Debug, Deserialize, Clone, Default)] pub struct AppConfig { pub observability: OtelConfig, @@ -65,19 +83,48 @@ pub struct AppConfig { pub logger: LoggerConfig, } +const DEFAULT_CONFIG_FILE: &str = "obs"; + /// Loading the configuration file /// Supports TOML, YAML and .env formats, read in order by priority +/// +/// # Parameters +/// - `config_dir`: Configuration file path +/// +/// # Returns +/// Configuration information +/// +/// # Example +/// ``` +/// use rustfs_obs::AppConfig; +/// use rustfs_obs::load_config; +/// +/// let config = load_config(None); +/// ``` pub fn load_config(config_dir: Option) -> AppConfig { - let config_dir = config_dir.unwrap_or_else(|| { - env::current_dir() - .map(|path| path.to_string_lossy().to_string()) - .unwrap_or_else(|_| { - eprintln!("Warning: Failed to get current directory, using empty path"); - String::new() - }) - }); + let config_dir = if let Some(path) = config_dir { + // Use the provided path + let path = std::path::Path::new(&path); + if path.extension().is_some() { + // If path has extension, use it as is (extension will be added by Config::builder) + path.with_extension("").to_string_lossy().into_owned() + } else { + // If path is a directory, append the default config file name + path.to_string_lossy().into_owned() + } + } else { + // If no path provided, use current directory + default config file + match env::current_dir() { + Ok(dir) => dir.join(DEFAULT_CONFIG_FILE).to_string_lossy().into_owned(), + Err(_) => { + eprintln!("Warning: Failed to get current directory, using default config file"); + DEFAULT_CONFIG_FILE.to_string() + } + } + }; - println!("config_dir: {}", config_dir); + // Log using proper logging instead of println when possible + println!("Using config file base: {}", config_dir); let config = Config::builder() .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml)) diff --git a/packages/obs/src/entry/args.rs b/packages/obs/src/entry/args.rs new file mode 100644 index 000000000..1099398ac --- /dev/null +++ b/packages/obs/src/entry/args.rs @@ -0,0 +1,74 @@ +use crate::entry::ObjectVersion; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Args - defines the arguments for API operations +/// Args is used to define the arguments for API operations. +/// +/// # Example +/// ``` +/// use rustfs_obs::Args; +/// use std::collections::HashMap; +/// +/// let args = Args::new() +/// .set_bucket(Some("my-bucket".to_string())) +/// .set_object(Some("my-object".to_string())) +/// .set_version_id(Some("123".to_string())) +/// .set_metadata(Some(HashMap::new())); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +pub struct Args { + #[serde(rename = "bucket", skip_serializing_if = "Option::is_none")] + pub bucket: Option, + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] + pub version_id: Option, + #[serde(rename = "objects", skip_serializing_if = "Option::is_none")] + pub objects: Option>, + #[serde(rename = "metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} + +impl Args { + /// Create a new Args object + pub fn new() -> Self { + Args { + bucket: None, + object: None, + version_id: None, + objects: None, + metadata: None, + } + } + + /// Set the bucket + pub fn set_bucket(mut self, bucket: Option) -> Self { + self.bucket = bucket; + self + } + + /// Set the object + pub fn set_object(mut self, object: Option) -> Self { + self.object = object; + self + } + + /// Set the version ID + pub fn set_version_id(mut self, version_id: Option) -> Self { + self.version_id = version_id; + self + } + + /// Set the objects + pub fn set_objects(mut self, objects: Option>) -> Self { + self.objects = objects; + self + } + + /// Set the metadata + pub fn set_metadata(mut self, metadata: Option>) -> Self { + self.metadata = metadata; + self + } +} diff --git a/packages/obs/src/entry/audit.rs b/packages/obs/src/entry/audit.rs index 8f8b1e34f..0aba2bac3 100644 --- a/packages/obs/src/entry/audit.rs +++ b/packages/obs/src/entry/audit.rs @@ -1,11 +1,64 @@ -use crate::ObjectVersion; +use crate::{BaseLogEntry, LogRecord, ObjectVersion}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; /// API details structure -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +/// ApiDetails is used to define the details of an API operation +/// +/// The `ApiDetails` structure contains the following fields: +/// - `name` - the name of the API operation +/// - `bucket` - the bucket name +/// - `object` - the object name +/// - `objects` - the list of objects +/// - `status` - the status of the API operation +/// - `status_code` - the status code of the API operation +/// - `input_bytes` - the input bytes +/// - `output_bytes` - the output bytes +/// - `header_bytes` - the header bytes +/// - `time_to_first_byte` - the time to first byte +/// - `time_to_first_byte_in_ns` - the time to first byte in nanoseconds +/// - `time_to_response` - the time to response +/// - `time_to_response_in_ns` - the time to response in nanoseconds +/// +/// The `ApiDetails` structure contains the following methods: +/// - `new` - create a new `ApiDetails` with default values +/// - `set_name` - set the name +/// - `set_bucket` - set the bucket +/// - `set_object` - set the object +/// - `set_objects` - set the objects +/// - `set_status` - set the status +/// - `set_status_code` - set the status code +/// - `set_input_bytes` - set the input bytes +/// - `set_output_bytes` - set the output bytes +/// - `set_header_bytes` - set the header bytes +/// - `set_time_to_first_byte` - set the time to first byte +/// - `set_time_to_first_byte_in_ns` - set the time to first byte in nanoseconds +/// - `set_time_to_response` - set the time to response +/// - `set_time_to_response_in_ns` - set the time to response in nanoseconds +/// +/// # Example +/// ``` +/// use rustfs_obs::ApiDetails; +/// use rustfs_obs::ObjectVersion; +/// +/// let api = ApiDetails::new() +/// .set_name(Some("GET".to_string())) +/// .set_bucket(Some("my-bucket".to_string())) +/// .set_object(Some("my-object".to_string())) +/// .set_objects(vec![ObjectVersion::new_with_object_name("my-object".to_string())]) +/// .set_status(Some("OK".to_string())) +/// .set_status_code(Some(200)) +/// .set_input_bytes(100) +/// .set_output_bytes(200) +/// .set_header_bytes(Some(50)) +/// .set_time_to_first_byte(Some("100ms".to_string())) +/// .set_time_to_first_byte_in_ns(Some("100000000ns".to_string())) +/// .set_time_to_response(Some("200ms".to_string())) +/// .set_time_to_response_in_ns(Some("200000000ns".to_string())); +/// ``` +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub struct ApiDetails { #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, @@ -135,22 +188,85 @@ impl ApiDetails { } /// Entry - audit entry logs +/// AuditLogEntry is used to define the structure of an audit log entry +/// +/// The `AuditLogEntry` structure contains the following fields: +/// - `base` - the base log entry +/// - `version` - the version of the audit log entry +/// - `deployment_id` - the deployment ID +/// - `event` - the event +/// - `entry_type` - the type of audit message +/// - `api` - the API details +/// - `remote_host` - the remote host +/// - `user_agent` - the user agent +/// - `req_path` - the request path +/// - `req_host` - the request host +/// - `req_claims` - the request claims +/// - `req_query` - the request query +/// - `req_header` - the request header +/// - `resp_header` - the response header +/// - `access_key` - the access key +/// - `parent_user` - the parent user +/// - `error` - the error +/// +/// The `AuditLogEntry` structure contains the following methods: +/// - `new` - create a new `AuditEntry` with default values +/// - `new_with_values` - create a new `AuditEntry` with version, time, event and api details +/// - `with_base` - set the base log entry +/// - `set_version` - set the version +/// - `set_deployment_id` - set the deployment ID +/// - `set_event` - set the event +/// - `set_entry_type` - set the entry type +/// - `set_api` - set the API details +/// - `set_remote_host` - set the remote host +/// - `set_user_agent` - set the user agent +/// - `set_req_path` - set the request path +/// - `set_req_host` - set the request host +/// - `set_req_claims` - set the request claims +/// - `set_req_query` - set the request query +/// - `set_req_header` - set the request header +/// - `set_resp_header` - set the response header +/// - `set_access_key` - set the access key +/// - `set_parent_user` - set the parent user +/// - `set_error` - set the error +/// +/// # Example +/// ``` +/// use rustfs_obs::AuditLogEntry; +/// use rustfs_obs::ApiDetails; +/// use std::collections::HashMap; +/// +/// let entry = AuditLogEntry::new() +/// .set_version("1.0".to_string()) +/// .set_deployment_id(Some("123".to_string())) +/// .set_event("event".to_string()) +/// .set_entry_type(Some("type".to_string())) +/// .set_api(ApiDetails::new()) +/// .set_remote_host(Some("remote-host".to_string())) +/// .set_user_agent(Some("user-agent".to_string())) +/// .set_req_path(Some("req-path".to_string())) +/// .set_req_host(Some("req-host".to_string())) +/// .set_req_claims(Some(HashMap::new())) +/// .set_req_query(Some(HashMap::new())) +/// .set_req_header(Some(HashMap::new())) +/// .set_resp_header(Some(HashMap::new())) +/// .set_access_key(Some("access-key".to_string())) +/// .set_parent_user(Some("parent-user".to_string())) +/// .set_error(Some("error".to_string())); #[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct AuditEntry { +pub struct AuditLogEntry { + #[serde(flatten)] + pub base: BaseLogEntry, pub version: String, #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")] pub deployment_id: Option, - pub time: DateTime, pub event: String, - // Class of audit message - S3, admin ops, bucket management #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub entry_type: Option, pub api: ApiDetails, #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")] pub remote_host: Option, - #[serde(rename = "requestID", skip_serializing_if = "Option::is_none")] - pub request_id: Option, #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")] pub user_agent: Option, #[serde(rename = "requestPath", skip_serializing_if = "Option::is_none")] @@ -165,8 +281,6 @@ pub struct AuditEntry { pub req_header: Option>, #[serde(rename = "responseHeader", skip_serializing_if = "Option::is_none")] pub resp_header: Option>, - #[serde(rename = "tags", skip_serializing_if = "Option::is_none")] - pub tags: Option>, #[serde(rename = "accessKey", skip_serializing_if = "Option::is_none")] pub access_key: Option, #[serde(rename = "parentUser", skip_serializing_if = "Option::is_none")] @@ -175,18 +289,17 @@ pub struct AuditEntry { pub error: Option, } -impl AuditEntry { +impl AuditLogEntry { /// Create a new `AuditEntry` with default values pub fn new() -> Self { - AuditEntry { + AuditLogEntry { + base: BaseLogEntry::new(), version: String::new(), deployment_id: None, - time: Utc::now(), event: String::new(), entry_type: None, api: ApiDetails::new(), remote_host: None, - request_id: None, user_agent: None, req_path: None, req_host: None, @@ -194,47 +307,25 @@ impl AuditEntry { req_query: None, req_header: None, resp_header: None, - tags: None, access_key: None, parent_user: None, error: None, } } - /// Create a new `AuditEntry` with version and time event and api details - /// # Arguments - /// * `version` - Version of the audit entry - /// * `time` - Time of the audit entry - /// * `event` - Event of the audit entry - /// * `api` - API details of the audit entry - /// # Returns - /// * `AuditEntry` with the given values - /// # Example - /// ``` - /// use chrono::Utc; - /// use rustfs_obs::{ApiDetails, AuditEntry}; - /// let entry = AuditEntry::new_with_values( - /// "v1".to_string(), - /// Utc::now(), - /// "event".to_string(), - /// ApiDetails::new(), - /// ); - /// ``` - /// # Remarks - /// This is a convenience method to create an `AuditEntry` with the given values - /// without having to set each field individually - /// This is useful when you want to create an `AuditEntry` with the given values - /// without having to set each field individually + /// Create a new `AuditEntry` with version, time, event and api details pub fn new_with_values(version: String, time: DateTime, event: String, api: ApiDetails) -> Self { - AuditEntry { + let mut base = BaseLogEntry::new(); + base.timestamp = time; + + AuditLogEntry { + base, version, deployment_id: None, - time, event, entry_type: None, api, remote_host: None, - request_id: None, user_agent: None, req_path: None, req_host: None, @@ -242,13 +333,18 @@ impl AuditEntry { req_query: None, req_header: None, resp_header: None, - tags: None, access_key: None, parent_user: None, error: None, } } + /// Set the base log entry + pub fn with_base(mut self, base: BaseLogEntry) -> Self { + self.base = base; + self + } + /// Set the version pub fn set_version(mut self, version: String) -> Self { self.version = version; @@ -261,12 +357,6 @@ impl AuditEntry { self } - /// Set the time - pub fn set_time(mut self, time: DateTime) -> Self { - self.time = time; - self - } - /// Set the event pub fn set_event(mut self, event: String) -> Self { self.event = event; @@ -291,12 +381,6 @@ impl AuditEntry { self } - /// Set the request ID - pub fn set_request_id(mut self, request_id: Option) -> Self { - self.request_id = request_id; - self - } - /// Set the user agent pub fn set_user_agent(mut self, user_agent: Option) -> Self { self.user_agent = user_agent; @@ -339,12 +423,6 @@ impl AuditEntry { self } - /// Set the tags - pub fn set_tags(mut self, tags: Option>) -> Self { - self.tags = tags; - self - } - /// Set the access key pub fn set_access_key(mut self, access_key: Option) -> Self { self.access_key = access_key; @@ -364,79 +442,12 @@ impl AuditEntry { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_audit_entry() { - let entry = AuditEntry::new() - .set_version("v1".to_string()) - .set_deployment_id(Some("12345".to_string())) - .set_time(Utc::now()) - .set_event("event".to_string()) - .set_entry_type(Some("type".to_string())) - .set_api(ApiDetails::new()) - .set_remote_host(Some("localhost".to_string())) - .set_request_id(Some("req-12345".to_string())) - .set_user_agent(Some("user-agent".to_string())) - .set_req_path(Some("/path".to_string())) - .set_req_host(Some("localhost".to_string())) - .set_req_claims(Some(HashMap::new())) - .set_req_query(Some(HashMap::new())) - .set_req_header(Some(HashMap::new())) - .set_resp_header(Some(HashMap::new())) - .set_tags(Some(HashMap::new())) - .set_access_key(Some("access-key".to_string())) - .set_parent_user(Some("parent-user".to_string())) - .set_error(Some("error".to_string())); - - assert_eq!(entry.version, "v1"); - assert_eq!(entry.deployment_id, Some("12345".to_string())); - assert_eq!(entry.event, "event"); - assert_eq!(entry.entry_type, Some("type".to_string())); - assert_eq!(entry.remote_host, Some("localhost".to_string())); - assert_eq!(entry.request_id, Some("req-12345".to_string())); - assert_eq!(entry.user_agent, Some("user-agent".to_string())); - assert_eq!(entry.req_path, Some("/path".to_string())); - assert_eq!(entry.req_host, Some("localhost".to_string())); - assert_eq!(entry.access_key, Some("access-key".to_string())); - assert_eq!(entry.parent_user, Some("parent-user".to_string())); - assert_eq!(entry.error, Some("error".to_string())); +impl LogRecord for AuditLogEntry { + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| String::from("{}")) } - #[test] - fn test_api_details() { - let api = ApiDetails::new() - .set_name(Some("name".to_string())) - .set_bucket(Some("bucket".to_string())) - .set_object(Some("object".to_string())) - .set_objects(vec![ObjectVersion { - object_name: "object".to_string(), - version_id: Some("12345".to_string()), - }]) - .set_status(Some("status".to_string())) - .set_status_code(Some(200)) - .set_input_bytes(100) - .set_output_bytes(200) - .set_header_bytes(Some(300)) - .set_time_to_first_byte(Some("100ms".to_string())) - .set_time_to_first_byte_in_ns(Some("100ns".to_string())) - .set_time_to_response(Some("200ms".to_string())) - .set_time_to_response_in_ns(Some("200ns".to_string())); - - assert_eq!(api.name, Some("name".to_string())); - assert_eq!(api.bucket, Some("bucket".to_string())); - assert_eq!(api.object, Some("object".to_string())); - assert_eq!(api.objects.len(), 1); - assert_eq!(api.status, Some("status".to_string())); - assert_eq!(api.status_code, Some(200)); - assert_eq!(api.input_bytes, 100); - assert_eq!(api.output_bytes, 200); - assert_eq!(api.header_bytes, Some(300)); - assert_eq!(api.time_to_first_byte, Some("100ms".to_string())); - assert_eq!(api.time_to_first_byte_in_ns, Some("100ns".to_string())); - assert_eq!(api.time_to_response, Some("200ms".to_string())); - assert_eq!(api.time_to_response_in_ns, Some("200ns".to_string())); + fn get_timestamp(&self) -> DateTime { + self.base.timestamp } } diff --git a/packages/obs/src/entry/base.rs b/packages/obs/src/entry/base.rs index c629c74d8..422646e09 100644 --- a/packages/obs/src/entry/base.rs +++ b/packages/obs/src/entry/base.rs @@ -3,490 +3,90 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -/// ObjectVersion object version key/versionId +/// Base log entry structure shared by all log types +/// This structure is used to serialize log entries to JSON +/// and send them to the log sinks +/// This structure is also used to deserialize log entries from JSON +/// This structure is also used to store log entries in the database +/// This structure is also used to query log entries from the database +/// +/// The `BaseLogEntry` structure contains the following fields: +/// - `timestamp` - the timestamp of the log entry +/// - `request_id` - the request ID of the log entry +/// - `message` - the message of the log entry +/// - `tags` - the tags of the log entry +/// +/// The `BaseLogEntry` structure contains the following methods: +/// - `new` - create a new `BaseLogEntry` with default values +/// - `message` - set the message +/// - `request_id` - set the request ID +/// - `tags` - set the tags +/// - `timestamp` - set the timestamp +/// +/// # Example +/// ``` +/// use rustfs_obs::BaseLogEntry; +/// use chrono::{DateTime, Utc}; +/// use std::collections::HashMap; +/// +/// let timestamp = Utc::now(); +/// let request = Some("req-123".to_string()); +/// let message = Some("This is a log message".to_string()); +/// let tags = Some(HashMap::new()); +/// +/// let entry = BaseLogEntry::new() +/// .timestamp(timestamp) +/// .request_id(request) +/// .message(message) +/// .tags(tags); +/// ``` #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)] -pub struct ObjectVersion { - #[serde(rename = "objectName")] - pub object_name: String, - #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] - pub version_id: Option, -} - -impl ObjectVersion { - /// Create a new ObjectVersion object - pub fn new() -> Self { - ObjectVersion { - object_name: String::new(), - version_id: None, - } - } - - /// Create a new ObjectVersion object with object name - pub fn new_with_object_name(object_name: String) -> Self { - ObjectVersion { - object_name, - version_id: None, - } - } - /// Set the object name - pub fn set_object_name(mut self, object_name: String) -> Self { - self.object_name = object_name; - self - } - - /// Set the version ID - pub fn set_version_id(mut self, version_id: Option) -> Self { - self.version_id = version_id; - self - } -} - -/// Args - defines the arguments for the API -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct Args { - #[serde(rename = "bucket", skip_serializing_if = "Option::is_none")] - pub bucket: Option, - #[serde(rename = "object", skip_serializing_if = "Option::is_none")] - pub object: Option, - #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] - pub version_id: Option, - #[serde(rename = "objects", skip_serializing_if = "Option::is_none")] - pub objects: Option>, - #[serde(rename = "metadata", skip_serializing_if = "Option::is_none")] - pub metadata: Option>, -} - -impl Args { - /// Create a new Args object - pub fn new() -> Self { - Args { - bucket: None, - object: None, - version_id: None, - objects: None, - metadata: None, - } - } - - /// Set the bucket - pub fn set_bucket(mut self, bucket: Option) -> Self { - self.bucket = bucket; - self - } - - /// Set the object - pub fn set_object(mut self, object: Option) -> Self { - self.object = object; - self - } - - /// Set the version ID - pub fn set_version_id(mut self, version_id: Option) -> Self { - self.version_id = version_id; - self - } - - /// Set the objects - pub fn set_objects(mut self, objects: Option>) -> Self { - self.objects = objects; - self - } - - /// Set the metadata - pub fn set_metadata(mut self, metadata: Option>) -> Self { - self.metadata = metadata; - self - } -} - -/// Trace - defines the trace -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct Trace { - #[serde(rename = "message", skip_serializing_if = "Option::is_none")] - pub message: Option, - #[serde(rename = "source", skip_serializing_if = "Option::is_none")] - pub source: Option>, - #[serde(rename = "variables", skip_serializing_if = "Option::is_none")] - pub variables: Option>, -} - -impl Trace { - /// Create a new Trace object - pub fn new() -> Self { - Trace { - message: None, - source: None, - variables: None, - } - } - - /// Set the message - pub fn set_message(mut self, message: Option) -> Self { - self.message = message; - self - } - - /// Set the source - pub fn set_source(mut self, source: Option>) -> Self { - self.source = source; - self - } - - /// Set the variables - pub fn set_variables(mut self, variables: Option>) -> Self { - self.variables = variables; - self - } -} - -/// API - defines the api type and its args -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct API { - #[serde(rename = "name", skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(rename = "args", skip_serializing_if = "Option::is_none")] - pub args: Option, -} - -impl API { - /// Create a new API object - pub fn new() -> Self { - API { name: None, args: None } - } - - /// Set the name - pub fn set_name(mut self, name: Option) -> Self { - self.name = name; - self - } - - /// Set the args - pub fn set_args(mut self, args: Option) -> Self { - self.args = args; - self - } -} - -/// Log kind/level enum -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum LogKind { - #[serde(rename = "INFO")] - Info, - #[serde(rename = "WARNING")] - Warning, - #[serde(rename = "ERROR")] - Error, - #[serde(rename = "FATAL")] - Fatal, -} - -impl Default for LogKind { - fn default() -> Self { - LogKind::Info - } -} - -/// Entry - defines fields and values of each log entry -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct Entry { - #[serde(rename = "site", skip_serializing_if = "Option::is_none")] - pub site: Option, - - #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")] - pub deployment_id: Option, - - pub level: LogKind, - - #[serde(rename = "errKind", skip_serializing_if = "Option::is_none")] - pub log_kind: Option, // Deprecated Jan 2024 - - pub time: DateTime, - - #[serde(rename = "api", skip_serializing_if = "Option::is_none")] - pub api: Option, - - #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")] - pub remote_host: Option, - - #[serde(rename = "host", skip_serializing_if = "Option::is_none")] - pub host: Option, +pub struct BaseLogEntry { + #[serde(rename = "time")] + pub timestamp: DateTime, #[serde(rename = "requestID", skip_serializing_if = "Option::is_none")] pub request_id: Option, - #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")] - pub user_agent: Option, - #[serde(rename = "message", skip_serializing_if = "Option::is_none")] pub message: Option, - #[serde(rename = "error", skip_serializing_if = "Option::is_none")] - pub trace: Option, + #[serde(rename = "tags", skip_serializing_if = "Option::is_none")] + pub tags: Option>, } -impl Entry { - /// Create a new Entry object with default values +impl BaseLogEntry { + /// Create a new BaseLogEntry with default values pub fn new() -> Self { - Entry { - site: None, - deployment_id: None, - level: LogKind::Info, - log_kind: None, - time: Utc::now(), - api: None, - remote_host: None, - host: None, + BaseLogEntry { + timestamp: Utc::now(), request_id: None, - user_agent: None, message: None, - trace: None, + tags: None, } } - /// Set the site - pub fn set_site(mut self, site: Option) -> Self { - self.site = site; - self - } - - /// Set the deployment ID - pub fn set_deployment_id(mut self, deployment_id: Option) -> Self { - self.deployment_id = deployment_id; - self - } - - /// Set the level - pub fn set_level(mut self, level: LogKind) -> Self { - self.level = level; - self - } - - /// Set the log kind - pub fn set_log_kind(mut self, log_kind: Option) -> Self { - self.log_kind = log_kind; - self - } - - /// Set the time - pub fn set_time(mut self, time: DateTime) -> Self { - self.time = time; - self - } - - /// Set the API - pub fn set_api(mut self, api: Option) -> Self { - self.api = api; - self - } - - /// Set the remote host - pub fn set_remote_host(mut self, remote_host: Option) -> Self { - self.remote_host = remote_host; - self - } - - /// Set the host - pub fn set_host(mut self, host: Option) -> Self { - self.host = host; - self - } - - /// Set the request ID - pub fn set_request_id(mut self, request_id: Option) -> Self { - self.request_id = request_id; - self - } - - /// Set the user agent - pub fn set_user_agent(mut self, user_agent: Option) -> Self { - self.user_agent = user_agent; - self - } - /// Set the message - pub fn set_message(mut self, message: Option) -> Self { + pub fn message(mut self, message: Option) -> Self { self.message = message; self } - /// Set the trace - pub fn set_trace(mut self, trace: Option) -> Self { - self.trace = trace; + /// Set the request ID + pub fn request_id(mut self, request_id: Option) -> Self { + self.request_id = request_id; + self + } + + /// Set the tags + pub fn tags(mut self, tags: Option>) -> Self { + self.tags = tags; + self + } + + /// Set the timestamp + pub fn timestamp(mut self, timestamp: DateTime) -> Self { + self.timestamp = timestamp; self } } - -/// Info holds console log messages -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Info { - #[serde(flatten)] - pub entry: Entry, - - pub console_msg: String, - - #[serde(rename = "node")] - pub node_name: String, - - #[serde(skip)] - pub err: Option, -} - -impl Info { - /// Create a new Info object with default values - pub fn new() -> Self { - Info { - entry: Entry::new(), - console_msg: String::new(), - node_name: String::new(), - err: None, - } - } - - /// Create a new Info object with console message and node name - pub fn new_with_console_msg(console_msg: String, node_name: String) -> Self { - Info { - entry: Entry::new(), - console_msg, - node_name, - err: None, - } - } - - /// Set the node name - pub fn set_node_name(&mut self, node_name: String) { - self.node_name = node_name; - } - - /// Set the entry - pub fn set_entry(&mut self, entry: Entry) { - self.entry = entry; - } - - /// Set the console message - pub fn set_console_msg(&mut self, console_msg: String) { - self.console_msg = console_msg; - } - - /// Set the error message - pub fn set_err(&mut self, err: Option) { - self.err = err; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_object_version() { - let mut object_version = ObjectVersion::new(); - object_version = object_version.clone().set_object_name("object".to_string()); - object_version = object_version.clone().set_version_id(Some("version".to_string())); - assert_eq!(object_version.object_name, "object".to_string()); - assert_eq!(object_version.version_id, Some("version".to_string())); - } - - #[test] - fn test_object_version_with_object_name() { - let object_version = ObjectVersion::new_with_object_name("object".to_string()); - assert_eq!(object_version.object_name, "object".to_string()); - assert_eq!(object_version.version_id, None); - } - - #[test] - fn test_args() { - let mut obj = ObjectVersion::new(); - obj.object_name = "object".to_string(); - obj.version_id = Some("version".to_string()); - let objs = vec![obj]; - let args = Args::new() - .set_bucket(Some("bucket".to_string())) - .set_object(Some("object".to_string())) - .set_version_id(Some("version".to_string())) - .set_objects(Some(objs.clone())) - .set_metadata(Some(HashMap::new())); - - assert_eq!(args.bucket, Some("bucket".to_string())); - assert_eq!(args.object, Some("object".to_string())); - assert_eq!(args.version_id, Some("version".to_string())); - assert_eq!(args.objects, Some(objs)); - assert_eq!(args.metadata, Some(HashMap::new())); - } - - #[test] - fn test_trace() { - let trace = Trace::new() - .set_message(Some("message".to_string())) - .set_source(Some(vec!["source".to_string()])) - .set_variables(Some(HashMap::new())); - - assert_eq!(trace.message, Some("message".to_string())); - assert_eq!(trace.source, Some(vec!["source".to_string()])); - assert_eq!(trace.variables, Some(HashMap::new())); - } - - #[test] - fn test_api() { - let api = API::new().set_name(Some("name".to_string())).set_args(Some(Args::new())); - - assert_eq!(api.name, Some("name".to_string())); - assert_eq!(api.args, Some(Args::new())); - } - - #[test] - fn test_log_kind() { - assert_eq!(LogKind::default(), LogKind::Info); - } - - #[test] - fn test_entry() { - let entry = Entry::new() - .set_site(Some("site".to_string())) - .set_deployment_id(Some("deployment_id".to_string())) - .set_level(LogKind::Info) - .set_log_kind(Some(LogKind::Info)) - .set_time(Utc::now()) - .set_api(Some(API::new())) - .set_remote_host(Some("remote_host".to_string())) - .set_host(Some("host".to_string())) - .set_request_id(Some("request_id".to_string())) - .set_user_agent(Some("user_agent".to_string())) - .set_message(Some("message".to_string())) - .set_trace(Some(Trace::new())); - - assert_eq!(entry.site, Some("site".to_string())); - assert_eq!(entry.deployment_id, Some("deployment_id".to_string())); - assert_eq!(entry.level, LogKind::Info); - assert_eq!(entry.log_kind, Some(LogKind::Info)); - assert_eq!(entry.api, Some(API::new())); - assert_eq!(entry.remote_host, Some("remote_host".to_string())); - assert_eq!(entry.host, Some("host".to_string())); - assert_eq!(entry.request_id, Some("request_id".to_string())); - assert_eq!(entry.user_agent, Some("user_agent".to_string())); - assert_eq!(entry.message, Some("message".to_string())); - assert_eq!(entry.trace, Some(Trace::new())); - } - - #[test] - fn test_info() { - let mut info = Info::new(); - info.set_node_name("node_name".to_string()); - info.set_entry(Entry::new()); - info.set_console_msg("console_msg".to_string()); - info.set_err(Some("err".to_string())); - - assert_eq!(info.node_name, "node_name".to_string()); - // assert_eq!(info.entry, Entry::new()); - assert_eq!(info.console_msg, "console_msg".to_string()); - assert_eq!(info.err, Some("err".to_string())); - } - - #[test] - fn test_info_with_console_msg() { - let info = Info::new_with_console_msg("console_msg".to_string(), "node_name".to_string()); - - assert_eq!(info.node_name, "node_name".to_string()); - assert_eq!(info.console_msg, "console_msg".to_string()); - assert_eq!(info.err, None); - } -} diff --git a/packages/obs/src/entry/log.rs b/packages/obs/src/entry/log.rs deleted file mode 100644 index 6960fef21..000000000 --- a/packages/obs/src/entry/log.rs +++ /dev/null @@ -1,83 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::de::Error; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use tracing_core::Level; - -/// Wrapper for `tracing_core::Level` to implement `Serialize` and `Deserialize` -#[derive(Debug, Clone)] -pub struct SerializableLevel(pub Level); - -impl From for SerializableLevel { - fn from(level: Level) -> Self { - SerializableLevel(level) - } -} - -impl From for Level { - fn from(serializable_level: SerializableLevel) -> Self { - serializable_level.0 - } -} - -impl Serialize for SerializableLevel { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(self.0.as_str()) - } -} - -impl<'de> Deserialize<'de> for SerializableLevel { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "TRACE" => Ok(SerializableLevel(Level::TRACE)), - "DEBUG" => Ok(SerializableLevel(Level::DEBUG)), - "INFO" => Ok(SerializableLevel(Level::INFO)), - "WARN" => Ok(SerializableLevel(Level::WARN)), - "ERROR" => Ok(SerializableLevel(Level::ERROR)), - _ => Err(D::Error::custom("unknown log level")), - } - } -} - -/// Server log entry structure -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct LogEntry { - pub timestamp: DateTime, // Log timestamp - pub level: SerializableLevel, // Log Level - pub message: String, // Log messages - pub source: String, // Log source (such as module name) - pub target: Option, // Log target - pub request_id: Option, // Request ID (Common Server Fields) - pub user_id: Option, // User ID (Common Server Fields) - pub fields: Vec<(String, String)>, // Attached fields (key value pairs) -} - -impl LogEntry { - /// Create a new LogEntry - pub fn new( - level: Level, - message: String, - source: String, - target: Option, - request_id: Option, - user_id: Option, - fields: Vec<(String, String)>, - ) -> Self { - LogEntry { - timestamp: Utc::now(), - level: SerializableLevel::from(level), - message, - source, - target, - request_id, - user_id, - fields, - } - } -} diff --git a/packages/obs/src/entry/mod.rs b/packages/obs/src/entry/mod.rs index 51ba15848..72dae2611 100644 --- a/packages/obs/src/entry/mod.rs +++ b/packages/obs/src/entry/mod.rs @@ -1,3 +1,143 @@ +pub(crate) mod args; pub(crate) mod audit; pub(crate) mod base; -pub(crate) mod log; +pub(crate) mod unified; + +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use tracing_core::Level; + +/// ObjectVersion is used across multiple modules +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub struct ObjectVersion { + #[serde(rename = "name")] + pub object_name: String, + #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] + pub version_id: Option, +} + +impl ObjectVersion { + /// Create a new ObjectVersion object + pub fn new() -> Self { + ObjectVersion { + object_name: String::new(), + version_id: None, + } + } + + /// Create a new ObjectVersion with object name + pub fn new_with_object_name(object_name: String) -> Self { + ObjectVersion { + object_name, + version_id: None, + } + } + + /// Set the object name + pub fn set_object_name(mut self, object_name: String) -> Self { + self.object_name = object_name; + self + } + + /// Set the version ID + pub fn set_version_id(mut self, version_id: Option) -> Self { + self.version_id = version_id; + self + } +} + +/// Log kind/level enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum LogKind { + #[serde(rename = "INFO")] + Info, + #[serde(rename = "WARNING")] + Warning, + #[serde(rename = "ERROR")] + Error, + #[serde(rename = "FATAL")] + Fatal, +} + +impl Default for LogKind { + fn default() -> Self { + LogKind::Info + } +} + +/// Trait for types that can be serialized to JSON and have a timestamp +/// This trait is used by `ServerLogEntry` to convert the log entry to JSON +/// and get the timestamp of the log entry +/// This trait is implemented by `ServerLogEntry` +/// +/// # Example +/// ``` +/// use rustfs_obs::LogRecord; +/// use chrono::{DateTime, Utc}; +/// use rustfs_obs::ServerLogEntry; +/// use tracing_core::Level; +/// +/// let log_entry = ServerLogEntry::new(Level::INFO, "api_handler".to_string()); +/// let json = log_entry.to_json(); +/// let timestamp = log_entry.get_timestamp(); +/// ``` +pub trait LogRecord { + fn to_json(&self) -> String; + fn get_timestamp(&self) -> chrono::DateTime; +} + +/// Wrapper for `tracing_core::Level` to implement `Serialize` and `Deserialize` +/// for `ServerLogEntry` +/// This is necessary because `tracing_core::Level` does not implement `Serialize` +/// and `Deserialize` +/// This is a workaround to allow `ServerLogEntry` to be serialized and deserialized +/// using `serde` +/// +/// # Example +/// ``` +/// use rustfs_obs::SerializableLevel; +/// use tracing_core::Level; +/// +/// let level = Level::INFO; +/// let serializable_level = SerializableLevel::from(level); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SerializableLevel(pub Level); + +impl From for SerializableLevel { + fn from(level: Level) -> Self { + SerializableLevel(level) + } +} + +impl From for Level { + fn from(serializable_level: SerializableLevel) -> Self { + serializable_level.0 + } +} + +impl Serialize for SerializableLevel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} + +impl<'de> Deserialize<'de> for SerializableLevel { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "TRACE" => Ok(SerializableLevel(Level::TRACE)), + "DEBUG" => Ok(SerializableLevel(Level::DEBUG)), + "INFO" => Ok(SerializableLevel(Level::INFO)), + "WARN" => Ok(SerializableLevel(Level::WARN)), + "ERROR" => Ok(SerializableLevel(Level::ERROR)), + _ => Err(D::Error::custom("unknown log level")), + } + } +} diff --git a/packages/obs/src/entry/unified.rs b/packages/obs/src/entry/unified.rs new file mode 100644 index 000000000..598ebe792 --- /dev/null +++ b/packages/obs/src/entry/unified.rs @@ -0,0 +1,279 @@ +use crate::{AuditLogEntry, BaseLogEntry, LogKind, LogRecord, SerializableLevel}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use tracing_core::Level; + +/// Server log entry with structured fields +/// ServerLogEntry is used to log structured log entries from the server +/// +/// The `ServerLogEntry` structure contains the following fields: +/// - `base` - the base log entry +/// - `level` - the log level +/// - `source` - the source of the log entry +/// - `user_id` - the user ID +/// - `fields` - the structured fields of the log entry +/// +/// The `ServerLogEntry` structure contains the following methods: +/// - `new` - create a new `ServerLogEntry` with specified level and source +/// - `with_base` - set the base log entry +/// - `user_id` - set the user ID +/// - `fields` - set the fields +/// - `add_field` - add a field +/// +/// # Example +/// ``` +/// use rustfs_obs::ServerLogEntry; +/// use tracing_core::Level; +/// +/// let entry = ServerLogEntry::new(Level::INFO, "test_module".to_string()) +/// .user_id(Some("user-456".to_string())) +/// .add_field("operation".to_string(), "login".to_string()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ServerLogEntry { + #[serde(flatten)] + pub base: BaseLogEntry, + + pub level: SerializableLevel, + pub source: String, + + #[serde(rename = "userId", skip_serializing_if = "Option::is_none")] + pub user_id: Option, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub fields: Vec<(String, String)>, +} + +impl ServerLogEntry { + /// Create a new ServerLogEntry with specified level and source + pub fn new(level: Level, source: String) -> Self { + ServerLogEntry { + base: BaseLogEntry::new(), + level: SerializableLevel(level), + source, + user_id: None, + fields: Vec::new(), + } + } + + /// Set the base log entry + pub fn with_base(mut self, base: BaseLogEntry) -> Self { + self.base = base; + self + } + + /// Set the user ID + pub fn user_id(mut self, user_id: Option) -> Self { + self.user_id = user_id; + self + } + + /// Set fields + pub fn fields(mut self, fields: Vec<(String, String)>) -> Self { + self.fields = fields; + self + } + + /// Add a field + pub fn add_field(mut self, key: String, value: String) -> Self { + self.fields.push((key, value)); + self + } +} + +impl LogRecord for ServerLogEntry { + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| String::from("{}")) + } + + fn get_timestamp(&self) -> DateTime { + self.base.timestamp + } +} + +/// Console log entry structure +/// ConsoleLogEntry is used to log console log entries +/// The `ConsoleLogEntry` structure contains the following fields: +/// - `base` - the base log entry +/// - `level` - the log level +/// - `console_msg` - the console message +/// - `node_name` - the node name +/// - `err` - the error message +/// The `ConsoleLogEntry` structure contains the following methods: +/// - `new` - create a new `ConsoleLogEntry` +/// - `new_with_console_msg` - create a new `ConsoleLogEntry` with console message and node name +/// - `with_base` - set the base log entry +/// - `set_level` - set the log level +/// - `set_node_name` - set the node name +/// - `set_console_msg` - set the console message +/// - `set_err` - set the error message +/// # Example +/// ``` +/// use rustfs_obs::ConsoleLogEntry; +/// +/// let entry = ConsoleLogEntry::new_with_console_msg("Test message".to_string(), "node-123".to_string()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsoleLogEntry { + #[serde(flatten)] + pub base: BaseLogEntry, + + pub level: LogKind, + pub console_msg: String, + pub node_name: String, + + #[serde(skip)] + pub err: Option, +} + +impl ConsoleLogEntry { + /// Create a new ConsoleLogEntry + pub fn new() -> Self { + ConsoleLogEntry { + base: BaseLogEntry::new(), + level: LogKind::Info, + console_msg: String::new(), + node_name: String::new(), + err: None, + } + } + + /// Create a new ConsoleLogEntry with console message and node name + pub fn new_with_console_msg(console_msg: String, node_name: String) -> Self { + ConsoleLogEntry { + base: BaseLogEntry::new(), + level: LogKind::Info, + console_msg, + node_name, + err: None, + } + } + + /// Set the base log entry + pub fn with_base(mut self, base: BaseLogEntry) -> Self { + self.base = base; + self + } + + /// Set the log level + pub fn set_level(mut self, level: LogKind) -> Self { + self.level = level; + self + } + + /// Set the node name + pub fn set_node_name(mut self, node_name: String) -> Self { + self.node_name = node_name; + self + } + + /// Set the console message + pub fn set_console_msg(mut self, console_msg: String) -> Self { + self.console_msg = console_msg; + self + } + + /// Set the error message + pub fn set_err(mut self, err: Option) -> Self { + self.err = err; + self + } +} + +impl LogRecord for ConsoleLogEntry { + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| String::from("{}")) + } + + fn get_timestamp(&self) -> DateTime { + self.base.timestamp + } +} + +/// Unified log entry type +/// UnifiedLogEntry is used to log different types of log entries +/// +/// The `UnifiedLogEntry` enum contains the following variants: +/// - `Server` - a server log entry +/// - `Audit` - an audit log entry +/// - `Console` - a console log entry +/// +/// The `UnifiedLogEntry` enum contains the following methods: +/// - `to_json` - convert the log entry to JSON +/// - `get_timestamp` - get the timestamp of the log entry +/// +/// # Example +/// ``` +/// use rustfs_obs::{UnifiedLogEntry, ServerLogEntry}; +/// use tracing_core::Level; +/// +/// let server_entry = ServerLogEntry::new(Level::INFO, "test_module".to_string()); +/// let unified = UnifiedLogEntry::Server(server_entry); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum UnifiedLogEntry { + #[serde(rename = "server")] + Server(ServerLogEntry), + + #[serde(rename = "audit")] + Audit(AuditLogEntry), + + #[serde(rename = "console")] + Console(ConsoleLogEntry), +} + +impl LogRecord for UnifiedLogEntry { + fn to_json(&self) -> String { + match self { + UnifiedLogEntry::Server(entry) => entry.to_json(), + UnifiedLogEntry::Audit(entry) => entry.to_json(), + UnifiedLogEntry::Console(entry) => entry.to_json(), + } + } + + fn get_timestamp(&self) -> DateTime { + match self { + UnifiedLogEntry::Server(entry) => entry.get_timestamp(), + UnifiedLogEntry::Audit(entry) => entry.get_timestamp(), + UnifiedLogEntry::Console(entry) => entry.get_timestamp(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base_log_entry() { + let base = BaseLogEntry::new() + .request_id(Some("req-123".to_string())) + .message(Some("Test message".to_string())); + + assert_eq!(base.request_id, Some("req-123".to_string())); + assert_eq!(base.message, Some("Test message".to_string())); + } + + #[test] + fn test_server_log_entry() { + let entry = ServerLogEntry::new(Level::INFO, "test_module".to_string()) + .user_id(Some("user-456".to_string())) + .add_field("operation".to_string(), "login".to_string()); + + assert_eq!(entry.level.0, Level::INFO); + assert_eq!(entry.source, "test_module"); + assert_eq!(entry.user_id, Some("user-456".to_string())); + assert_eq!(entry.fields.len(), 1); + assert_eq!(entry.fields[0], ("operation".to_string(), "login".to_string())); + } + + #[test] + fn test_unified_log_entry_json() { + let server_entry = ServerLogEntry::new(Level::INFO, "test_source".to_string()); + let unified = UnifiedLogEntry::Server(server_entry); + + let json = unified.to_json(); + assert!(json.contains("test_source")); + } +} diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index 8d00c7b09..0db02853f 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -3,6 +3,30 @@ /// `obs` is a logging and observability library for Rust. /// It provides a simple and easy-to-use interface for logging and observability. /// It is built on top of the `log` crate and `opentelemetry` crate. +/// +/// ## Features +/// - Structured logging +/// - Distributed tracing +/// - Metrics collection +/// - Log processing worker +/// - Multiple sinks +/// - Configuration-based setup +/// - Telemetry guard +/// - Global logger +/// - Log levels +/// - Log entry types +/// - Log record +/// - Object version +/// - Local IP address +/// +/// ## Usage +/// +/// ```rust +/// use rustfs_obs::{AppConfig, init_obs}; +/// +/// let config = AppConfig::default(); +/// let (logger, guard) = init_obs(config); +/// ``` mod config; mod entry; mod logger; @@ -13,9 +37,11 @@ mod worker; pub use config::load_config; pub use config::{AppConfig, OtelConfig}; -pub use entry::audit::{ApiDetails, AuditEntry}; -pub use entry::base::{Args, Entry, Info, LogKind, ObjectVersion, Trace, API}; -pub use entry::log::{LogEntry, SerializableLevel}; +pub use entry::args::Args; +pub use entry::audit::{ApiDetails, AuditLogEntry}; +pub use entry::base::BaseLogEntry; +pub use entry::unified::{ConsoleLogEntry, ServerLogEntry, UnifiedLogEntry}; +pub use entry::{LogKind, LogRecord, ObjectVersion, SerializableLevel}; pub use logger::start_logger; pub use logger::{ ensure_logger_initialized, get_global_logger, init_global_logger, locked_logger, log_debug, log_error, log_info, log_trace, diff --git a/packages/obs/src/logger.rs b/packages/obs/src/logger.rs index a3848cc3b..22914a666 100644 --- a/packages/obs/src/logger.rs +++ b/packages/obs/src/logger.rs @@ -1,4 +1,4 @@ -use crate::{AppConfig, LogEntry, SerializableLevel, Sink}; +use crate::{AppConfig, AuditLogEntry, BaseLogEntry, ConsoleLogEntry, ServerLogEntry, Sink, UnifiedLogEntry}; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::sync::{Mutex, OnceCell}; @@ -10,14 +10,14 @@ static GLOBAL_LOGGER: OnceCell>> = OnceCell::const_new(); /// Server log processor #[derive(Debug)] pub struct Logger { - sender: Sender, // Log sending channel + sender: Sender, // Log sending channel queue_capacity: usize, } impl Logger { /// Create a new Logger instance /// Returns Logger and corresponding Receiver - pub fn new(config: &AppConfig) -> (Self, Receiver) { + pub fn new(config: &AppConfig) -> (Self, Receiver) { // Get queue capacity from configuration, or use default values 10000 let queue_capacity = config.logger.queue_capacity.unwrap_or(10000); let (sender, receiver) = mpsc::channel(queue_capacity); @@ -39,83 +39,80 @@ impl Logger { self.queue_capacity } - /// Asynchronous logging of server logs - /// Attach the log to the current Span and generate a separate Tracing Event - #[tracing::instrument(skip(self), fields(log_source = "logger"))] - pub async fn log(&self, entry: LogEntry) -> Result<(), LogError> { - // Log messages to the current Span - tracing::Span::current() - .record("log_message", &entry.message) - .record("source", &entry.source); - println!("target start is {:?}", &entry.target); - let target = if let Some(target) = &entry.target { - target.clone() - } else { - "server_logs".to_string() - }; + /// Log a server entry + #[tracing::instrument(skip(self), fields(log_source = "logger_server"))] + pub async fn log_server_entry(&self, entry: ServerLogEntry) -> Result<(), LogError> { + self.log_entry(UnifiedLogEntry::Server(entry)).await + } - println!("target end is {:?}", target); - // Generate independent Tracing Events with full LogEntry information - // Generate corresponding events according to level - match entry.level { - SerializableLevel(Level::ERROR) => { - tracing::error!( - target = %target.clone(), - timestamp = %entry.timestamp, - message = %entry.message, - source = %entry.source, - request_id = ?entry.request_id, - user_id = ?entry.user_id, - fields = ?entry.fields - ); + /// Log an audit entry + #[tracing::instrument(skip(self), fields(log_source = "logger_audit"))] + pub async fn log_audit_entry(&self, entry: AuditLogEntry) -> Result<(), LogError> { + self.log_entry(UnifiedLogEntry::Audit(entry)).await + } + + /// Log a console entry + #[tracing::instrument(skip(self), fields(log_source = "logger_console"))] + pub async fn log_console_entry(&self, entry: ConsoleLogEntry) -> Result<(), LogError> { + self.log_entry(UnifiedLogEntry::Console(entry)).await + } + + /// Asynchronous logging of unified log entries + #[tracing::instrument(skip(self), fields(log_source = "logger"))] + pub async fn log_entry(&self, entry: UnifiedLogEntry) -> Result<(), LogError> { + // Extract information for tracing based on entry type + match &entry { + UnifiedLogEntry::Server(server) => { + tracing::Span::current() + .record("log_level", &server.level.0.as_str()) + .record("log_message", &server.base.message.as_deref().unwrap_or("")) + .record("source", &server.source); + + // Generate tracing event based on log level + match server.level.0 { + Level::ERROR => { + tracing::error!(target: "server_logs", message = %server.base.message.as_deref().unwrap_or("")); + } + Level::WARN => { + tracing::warn!(target: "server_logs", message = %server.base.message.as_deref().unwrap_or("")); + } + Level::INFO => { + tracing::info!(target: "server_logs", message = %server.base.message.as_deref().unwrap_or("")); + } + Level::DEBUG => { + tracing::debug!(target: "server_logs", message = %server.base.message.as_deref().unwrap_or("")); + } + Level::TRACE => { + tracing::trace!(target: "server_logs", message = %server.base.message.as_deref().unwrap_or("")); + } + } } - SerializableLevel(Level::WARN) => { - tracing::warn!( - target = %target.clone(), - timestamp = %entry.timestamp, - message = %entry.message, - source = %entry.source, - request_id = ?entry.request_id, - user_id = ?entry.user_id, - fields = ?entry.fields - ); - } - SerializableLevel(Level::INFO) => { + UnifiedLogEntry::Audit(audit) => { tracing::info!( - target = %target.clone(), - timestamp = %entry.timestamp, - message = %entry.message, - source = %entry.source, - request_id = ?entry.request_id, - user_id = ?entry.user_id, - fields = ?entry.fields + target: "audit_logs", + event = %audit.event, + api = %audit.api.name.as_deref().unwrap_or("unknown"), + message = %audit.base.message.as_deref().unwrap_or("") ); } - SerializableLevel(Level::DEBUG) => { - tracing::debug!( - target = %target.clone(), - timestamp = %entry.timestamp, - message = %entry.message, - source = %entry.source, - request_id = ?entry.request_id, - user_id = ?entry.user_id, - fields = ?entry.fields - ); - } - SerializableLevel(Level::TRACE) => { - tracing::trace!( - target = %target.clone(), - timestamp = %entry.timestamp, - message = %entry.message, - source = %entry.source, - request_id = ?entry.request_id, - user_id = ?entry.user_id, - fields = ?entry.fields + UnifiedLogEntry::Console(console) => { + let level_str = match console.level { + crate::LogKind::Info => "INFO", + crate::LogKind::Warning => "WARN", + crate::LogKind::Error => "ERROR", + crate::LogKind::Fatal => "FATAL", + }; + + tracing::info!( + target: "console_logs", + level = %level_str, + node = %console.node_name, + message = %console.console_msg ); } } - // Send logs to asynchronous queues to improve error handling + // Send logs to async queue with improved error handling match self.sender.try_send(entry) { Ok(_) => Ok(()), Err(mpsc::error::TrySendError::Full(entry)) => { @@ -150,28 +147,25 @@ impl Logger { /// use rustfs_obs::Logger; /// /// async fn example(logger: &Logger) { - /// let _ = logger.write_with_context("This is an information message", "example",Level::INFO, Some("target".to_string()),Some("req-12345".to_string()), Some("user-6789".to_string()), vec![("endpoint".to_string(), "/api/v1/data".to_string())]).await; + /// let _ = logger.write_with_context("This is an information message", "example",Level::INFO, Some("req-12345".to_string()), Some("user-6789".to_string()), vec![("endpoint".to_string(), "/api/v1/data".to_string())]).await; /// } pub async fn write_with_context( &self, message: &str, source: &str, level: Level, - target: Option, request_id: Option, user_id: Option, fields: Vec<(String, String)>, ) -> Result<(), LogError> { - self.log(LogEntry::new( - level, - message.to_string(), - source.to_string(), - target, - request_id, - user_id, - fields, - )) - .await + let base = BaseLogEntry::new().message(Some(message.to_string())).request_id(request_id); + + let server_entry = ServerLogEntry::new(level, source.to_string()) + .user_id(user_id) + .fields(fields) + .with_base(base); + + self.log_server_entry(server_entry).await } /// Write log @@ -194,16 +188,7 @@ impl Logger { /// } /// ``` pub async fn write(&self, message: &str, source: &str, level: Level) -> Result<(), LogError> { - self.log(LogEntry::new( - level, - message.to_string(), - source.to_string(), - None, - None, - None, - Vec::new(), - )) - .await + self.write_with_context(message, source, level, None, None, Vec::new()).await } /// Shutdown the logger @@ -458,7 +443,6 @@ pub async fn log_trace(message: &str, source: &str) -> Result<(), LogError> { /// - `message`: Message to be logged /// - `source`: Source of the log /// - `level`: Log level -/// - `target`: Log target /// - `request_id`: Request ID /// - `user_id`: User ID /// - `fields`: Additional fields @@ -470,14 +454,13 @@ pub async fn log_trace(message: &str, source: &str) -> Result<(), LogError> { /// use rustfs_obs::log_with_context; /// /// async fn example() { -/// let _ = log_with_context("This is an information message", "example", Level::INFO, Some("target".to_string()), Some("req-12345".to_string()), Some("user-6789".to_string()), vec![("endpoint".to_string(), "/api/v1/data".to_string())]).await; +/// let _ = log_with_context("This is an information message", "example", Level::INFO, Some("req-12345".to_string()), Some("user-6789".to_string()), vec![("endpoint".to_string(), "/api/v1/data".to_string())]).await; /// } /// ``` pub async fn log_with_context( message: &str, source: &str, level: Level, - target: Option, request_id: Option, user_id: Option, fields: Vec<(String, String)>, @@ -485,6 +468,6 @@ pub async fn log_with_context( get_global_logger() .lock() .await - .write_with_context(message, source, level, target, request_id, user_id, fields) + .write_with_context(message, source, level, request_id, user_id, fields) .await } diff --git a/packages/obs/src/sink.rs b/packages/obs/src/sink.rs index 2363a69dd..8eb1830d7 100644 --- a/packages/obs/src/sink.rs +++ b/packages/obs/src/sink.rs @@ -1,4 +1,4 @@ -use crate::{AppConfig, LogEntry}; +use crate::{AppConfig, LogRecord, UnifiedLogEntry}; use async_trait::async_trait; use std::sync::Arc; use tokio::fs::OpenOptions; @@ -8,7 +8,7 @@ use tokio::io::AsyncWriteExt; /// Sink Trait definition, asynchronously write logs #[async_trait] pub trait Sink: Send + Sync { - async fn write(&self, entry: &LogEntry); + async fn write(&self, entry: &UnifiedLogEntry); } #[cfg(feature = "kafka")] @@ -18,7 +18,7 @@ pub struct KafkaSink { topic: String, batch_size: usize, batch_timeout_ms: u64, - entries: Arc>>, + entries: Arc>>, last_flush: Arc, } @@ -64,7 +64,7 @@ impl KafkaSink { async fn periodic_flush( producer: rdkafka::producer::FutureProducer, topic: String, - entries: Arc>>, + entries: Arc>>, last_flush: Arc, timeout_ms: u64, ) { @@ -88,7 +88,7 @@ impl KafkaSink { } } - async fn send_batch(producer: &rdkafka::producer::FutureProducer, topic: &str, entries: Vec) { + async fn send_batch(producer: &rdkafka::producer::FutureProducer, topic: &str, entries: Vec) { for entry in entries { let payload = match serde_json::to_string(&entry) { Ok(p) => p, @@ -98,7 +98,7 @@ impl KafkaSink { } }; - let span_id = entry.timestamp.to_rfc3339(); + let span_id = entry.get_timestamp().to_rfc3339(); let _ = producer .send( @@ -113,7 +113,7 @@ impl KafkaSink { #[cfg(feature = "kafka")] #[async_trait] impl Sink for KafkaSink { - async fn write(&self, entry: &LogEntry) { + async fn write(&self, entry: &UnifiedLogEntry) { let mut batch = self.entries.lock().await; batch.push(entry.clone()); @@ -129,7 +129,7 @@ impl Sink for KafkaSink { if should_flush_by_size || should_flush_by_time { // Existing flush logic - let entries_to_send: Vec = batch.drain(..).collect(); + let entries_to_send: Vec = batch.drain(..).collect(); let producer = self.producer.clone(); let topic = self.topic.clone(); @@ -203,7 +203,7 @@ impl WebhookSink { #[cfg(feature = "webhook")] #[async_trait] impl Sink for WebhookSink { - async fn write(&self, entry: &LogEntry) { + async fn write(&self, entry: &UnifiedLogEntry) { let mut retries = 0; let url = self.url.clone(); let entry_clone = entry.clone(); @@ -327,12 +327,17 @@ impl FileSink { #[cfg(feature = "file")] #[async_trait] impl Sink for FileSink { - async fn write(&self, entry: &LogEntry) { + async fn write(&self, entry: &UnifiedLogEntry) { let line = format!("{:?}\n", entry); let mut writer = self.writer.lock().await; if let Err(e) = writer.write_all(line.as_bytes()).await { - eprintln!("Failed to write log to file {}: {}", self.path, e); + eprintln!( + "Failed to write log to file {}: {},entry timestamp:{:?}", + self.path, + e, + entry.get_timestamp() + ); return; } diff --git a/packages/obs/src/worker.rs b/packages/obs/src/worker.rs index 1c40f47f5..2d7ee2e14 100644 --- a/packages/obs/src/worker.rs +++ b/packages/obs/src/worker.rs @@ -1,9 +1,9 @@ -use crate::{entry::log::LogEntry, sink::Sink}; +use crate::{sink::Sink, UnifiedLogEntry}; use std::sync::Arc; use tokio::sync::mpsc::Receiver; /// Start the log processing worker thread -pub async fn start_worker(receiver: Receiver, sinks: Vec>) { +pub async fn start_worker(receiver: Receiver, sinks: Vec>) { let mut receiver = receiver; while let Some(entry) = receiver.recv().await { for sink in &sinks { From 4795638763f97da6e59c0d0370c7797493b9bbef Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 18 Mar 2025 16:39:18 +0800 Subject: [PATCH 11/27] feat(observability): add obs_config option and document stdout export - Add obs_config parameter to config struct with default path - Document how to modify use_stdout value in README.md - Support configuring observability via file or environment variables This change helps users configure telemetry output destination for better observability options in different deployment scenarios. --- .docker/observability/README.md | 101 ++++++++++++++++++++++++++++++++ README.md | 45 ++++++++++++++ rustfs/src/config/mod.rs | 28 ++++++++- rustfs/src/main.rs | 9 +-- 4 files changed, 178 insertions(+), 5 deletions(-) diff --git a/.docker/observability/README.md b/.docker/observability/README.md index e69de29bb..f0bd03fcb 100644 --- a/.docker/observability/README.md +++ b/.docker/observability/README.md @@ -0,0 +1,101 @@ +# Observability + +This directory contains the observability stack for the application. The stack is composed of the following components: + +- Prometheus +- Grafana +- Loki +- Jaeger +- Otel Collector + +## Prometheus + +Prometheus is a monitoring and alerting toolkit. It scrapes metrics from instrumented jobs, either directly or via an +intermediary push gateway for short-lived jobs. It stores all scraped samples locally and runs rules over this data to +either aggregate and record new time series from existing data or generate alerts. Grafana or other API consumers can be +used to visualize the collected data. + +## Grafana + +Grafana is a multi-platform open-source analytics and interactive visualization web application. It provides charts, +graphs, and alerts for the web when connected to supported data sources. + +## Loki + +Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is +designed to be very cost-effective and easy to operate. It does not index the contents of the logs, but rather a set of +labels for each log stream. + +## Jaeger + +Jaeger is a distributed tracing system released as open source by Uber Technologies. It is used for monitoring and +troubleshooting microservices-based distributed systems, including: + +- Distributed context propagation +- Distributed transaction monitoring +- Root cause analysis +- Service dependency analysis +- Performance / latency optimization + +## Otel Collector + +The OpenTelemetry Collector offers a vendor-agnostic implementation on how to receive, process, and export telemetry +data. It removes the need to run, operate, and maintain multiple agents/collectors in order to support open-source +observability data formats (e.g. Jaeger, Prometheus, etc.) sending to one or more open-source or commercial back-ends. + +## How to use + +To deploy the observability stack, run the following command: + +```bash +docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d +``` + +To access the Grafana dashboard, navigate to `http://localhost:3000` in your browser. The default username and password +are `admin` and `admin`, respectively. + +To access the Jaeger dashboard, navigate to `http://localhost:16686` in your browser. + +To access the Prometheus dashboard, navigate to `http://localhost:9090` in your browser. + +## How to stop + +To stop the observability stack, run the following command: + +```bash +docker-compose -f docker-compose.yml -f docker-compose.override.yml down +``` + +## How to remove data + +To remove the data generated by the observability stack, run the following command: + +```bash +docker-compose -f docker-compose.yml -f docker-compose.override.yml down -v +``` + +## How to configure + +To configure the observability stack, modify the `docker-compose.override.yml` file. The file contains the following + +```yaml +services: + prometheus: + environment: + - PROMETHEUS_CONFIG_FILE=/etc/prometheus/prometheus.yml + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning +``` + +The `prometheus` service mounts the `prometheus.yml` file to `/etc/prometheus/prometheus.yml`. The `grafana` service +mounts the `grafana/provisioning` directory to `/etc/grafana/provisioning`. You can modify these files to configure the +observability stack. + + + diff --git a/README.md b/README.md index dbb7e9f6e..217ab3c14 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ export RUSTFS_VOLUMES="./target/volume/test" export RUSTFS_ADDRESS="0.0.0.0:9000" export RUSTFS_CONSOLE_ENABLE=true export RUSTFS_CONSOLE_ADDRESS="0.0.0.0:9001" +export RUSTFS_OBS_CONFIG="config/obs.toml" ``` You need replace your real data folder: @@ -47,3 +48,47 @@ You need replace your real data folder: ``` ./rustfs /data/rustfs ``` + +## How to deploy the observability stack + +The OpenTelemetry Collector offers a vendor-agnostic implementation on how to receive, process, and export telemetry +data. It removes the need to run, operate, and maintain multiple agents/collectors in order to support open-source +observability data formats (e.g. Jaeger, Prometheus, etc.) sending to one or more open-source or commercial back-ends. + +1. Enter the `.docker/observability` directory, +2. Run the following command: + +```bash +docker-compose -f docker-compose.yml up -d +``` + +3. Access the Grafana dashboard by navigating to `http://localhost:3000` in your browser. The default username and + password are `admin` and `admin`, respectively. + +4. Access the Jaeger dashboard by navigating to `http://localhost:16686` in your browser. + +5. Access the Prometheus dashboard by navigating to `http://localhost:9090` in your browser. + +## Create a new Observability configuration file + +#### 1. Enter the `config` directory, + +#### 2. Copy `obs.toml.example` to `obs.toml` + +#### 3. Modify the `obs.toml` configuration file + +##### 3.1. Modify the `endpoint` value to the address of the OpenTelemetry Collector + +##### 3.2. Modify the `service_name` value to the name of the service + +##### 3.3. Modify the `service_version` value to the version of the service + +##### 3.4. Modify the `deployment_environment` value to the environment of the service + +##### 3.5. Modify the `meter_interval` value to export interval + +##### 3.6. Modify the `sample_ratio` value to the sample ratio + +##### 3.7. Modify the `use_stdout` value to export to stdout + + diff --git a/rustfs/src/config/mod.rs b/rustfs/src/config/mod.rs index cd8f238fd..d6552af83 100644 --- a/rustfs/src/config/mod.rs +++ b/rustfs/src/config/mod.rs @@ -1,11 +1,32 @@ use clap::Parser; use const_str::concat; use ecstore::global::DEFAULT_PORT; +use std::string::ToString; shadow_rs::shadow!(build); +/// Default Access Key +/// Default value: rustfsadmin +/// Environment variable: RUSTFS_ACCESS_KEY +/// Command line argument: --access-key +/// Example: RUSTFS_ACCESS_KEY=rustfsadmin +/// Example: --access-key rustfsadmin pub const DEFAULT_ACCESS_KEY: &str = "rustfsadmin"; +/// Default Secret Key +/// Default value: rustfsadmin +/// Environment variable: RUSTFS_SECRET_KEY +/// Command line argument: --secret-key +/// Example: RUSTFS_SECRET_KEY=rustfsadmin +/// Example: --secret-key rustfsadmin pub const DEFAULT_SECRET_KEY: &str = "rustfsadmin"; +/// Default configuration file for observability +/// Default value: config/obs.toml +/// Environment variable: RUSTFS_OBS_CONFIG +/// Command line argument: --obs-config +/// Example: RUSTFS_OBS_CONFIG=config/obs.toml +/// Example: --obs-config config/obs.toml +/// Example: --obs-config /etc/rustfs/obs.toml +pub const DEFAULT_OBS_CONFIG: &str = "config/obs.toml"; #[allow(clippy::const_is_empty)] const SHORT_VERSION: &str = { @@ -31,7 +52,7 @@ const LONG_VERSION: &str = concat!( concat!("git status :\n", build::GIT_STATUS_FILE), ); -#[derive(Debug, Parser)] +#[derive(Debug, Parser, Clone)] #[command(version = SHORT_VERSION, long_version = LONG_VERSION)] pub struct Opt { /// DIR points to a directory on a filesystem. @@ -62,4 +83,9 @@ pub struct Opt { #[arg(long, default_value_t = format!("127.0.0.1:{}", 9002), env = "RUSTFS_CONSOLE_ADDRESS")] pub console_address: String, + + /// Observability configuration file + /// Default value: config/obs.toml + #[arg(long, default_value_t = DEFAULT_OBS_CONFIG.to_string(), env = "RUSTFS_OBS_CONFIG")] + pub obs_config: String, } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index b87b16ca3..1c1351768 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -92,9 +92,7 @@ fn main() -> Result<()> { //设置 trace // setup_tracing(); - let config = load_config(Some("packages/obs/examples/config".to_string())); - // Initialize the logger asynchronously - let (_logger, _guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); + //运行参数 run(opt) } @@ -102,6 +100,9 @@ fn main() -> Result<()> { #[tokio::main] async fn run(opt: config::Opt) -> Result<()> { debug!("opt: {:?}", &opt); + let config = load_config(Some(opt.clone().obs_config)); + // Initialize Observability + init_obs(config).await; let mut server_addr = net::check_local_server_addr(opt.address.as_str()).unwrap(); @@ -277,7 +278,7 @@ async fn run(opt: config::Opt) -> Result<()> { init_iam_sys(store.clone()).await.unwrap(); new_global_notification_sys(endpoint_pools.clone()).await.map_err(|err| { - error!("new_global_notification_sys faild {:?}", &err); + error!("new_global_notification_sys failed {:?}", &err); Error::from_string(err.to_string()) })?; From f1c6f276e6874ffcd16ca6295feb022dd85be331 Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 18 Mar 2025 19:04:00 +0800 Subject: [PATCH 12/27] improve code for main --- rustfs/src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 0dcf4094c..480b5db98 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -93,6 +93,10 @@ fn main() -> Result<()> { //设置 trace // setup_tracing(); + let config = load_config(Some(opt.clone().obs_config)); + // Initialize Observability + let (_logger, _guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); + //运行参数 run(opt) } @@ -100,9 +104,9 @@ fn main() -> Result<()> { #[tokio::main] async fn run(opt: config::Opt) -> Result<()> { debug!("opt: {:?}", &opt); - let config = load_config(Some(opt.clone().obs_config)); + // let config = load_config(Some(opt.clone().obs_config)); // Initialize Observability - init_obs(config).await; + // let (_logger, _guard) = init_obs(config).await; let mut server_addr = net::check_local_server_addr(opt.address.as_str()).unwrap(); From 197ae72e9315f4a62f67e9080addd83bcbd1a06a Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 18 Mar 2025 22:57:26 +0800 Subject: [PATCH 13/27] improve code for observability --- Cargo.lock | 1 + Cargo.toml | 1 + packages/obs/examples/config.toml | 2 +- packages/obs/examples/server.rs | 10 +++++++++- rustfs/Cargo.toml | 1 + rustfs/src/main.rs | 31 ++++++++++++++++++++++++------- 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4c5b173e..ef3176efb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5633,6 +5633,7 @@ dependencies = [ "mime", "mime_guess", "netif", + "once_cell", "pin-project-lite", "prost", "prost-build", diff --git a/Cargo.toml b/Cargo.toml index 989e7c4e2..e45251674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ lazy_static = "1.5.0" local-ip-address = "0.6.3" mime = "0.3.17" netif = "0.1.6" +once_cell = "1.21.1" opentelemetry = { version = "0.28" } opentelemetry-appender-tracing = { version = "0.28.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } opentelemetry_sdk = { version = "0.28" } diff --git a/packages/obs/examples/config.toml b/packages/obs/examples/config.toml index ddf376352..2ceac4397 100644 --- a/packages/obs/examples/config.toml +++ b/packages/obs/examples/config.toml @@ -1,6 +1,6 @@ [observability] endpoint = "http://localhost:4317" -use_stdout = false +use_stdout = true sample_ratio = 0.5 meter_interval = 30 service_name = "rustfs_obs_service" diff --git a/packages/obs/examples/server.rs b/packages/obs/examples/server.rs index 2d35c8d4f..15a64c8be 100644 --- a/packages/obs/examples/server.rs +++ b/packages/obs/examples/server.rs @@ -1,5 +1,6 @@ use opentelemetry::global; -use rustfs_obs::{get_logger, init_obs, load_config, log_info, ServerLogEntry}; +use rustfs_obs::{get_logger, init_obs, load_config, log_info, BaseLogEntry, ServerLogEntry}; +use std::collections::HashMap; use std::time::{Duration, SystemTime}; use tracing::{info, instrument}; use tracing_core::Level; @@ -34,7 +35,14 @@ async fn run(bucket: String, object: String, user: String, service_name: String) &[opentelemetry::KeyValue::new("operation", "run")], ); + let base_entry = BaseLogEntry::new() + .message(Some("run logger api_handler info".to_string())) + .request_id(Some("request_id".to_string())) + .timestamp(chrono::DateTime::from(start_time)) + .tags(Some(HashMap::default())); + let server_entry = ServerLogEntry::new(Level::INFO, "api_handler".to_string()) + .with_base(base_entry) .user_id(Some(user.clone())) .add_field("operation".to_string(), "login".to_string()) .add_field("bucket".to_string(), bucket.clone()) diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 73e53c0bb..530cc57ae 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -33,6 +33,7 @@ http-body.workspace = true lock.workspace = true mime.workspace = true netif.workspace = true +once_cell.workspace = true pin-project-lite.workspace = true prost.workspace = true prost-types.workspace = true diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 480b5db98..6f4fee1ee 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -34,6 +34,7 @@ use hyper_util::{ service::TowerToHyperService, }; use iam::init_iam_sys; +use once_cell::sync::OnceCell; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; use rustfs_obs::{init_obs, load_config}; use s3s::{host::MultiDomain, service::S3ServiceBuilder}; @@ -46,6 +47,20 @@ use tracing::{debug, error, info, warn}; use tracing_error::ErrorLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +/// Guard that holds a reference to the global observability subsystem. +/// This struct ensures that tracing/logging components remain alive for the +/// application's lifetime. The underlying guard is dropped when this struct +/// is dropped, which may flush logs or perform cleanup operations. +#[allow(dead_code)] +struct TracingGuard(Box); + +impl Drop for TracingGuard { + fn drop(&mut self) { + debug!("Dropping global tracing guard, flushing logs"); + } +} +static GLOBAL_GUARD: OnceCell = OnceCell::new(); + #[allow(dead_code)] fn setup_tracing() { use tracing_subscriber::EnvFilter; @@ -87,26 +102,28 @@ fn print_server_info() { } fn main() -> Result<()> { - //解析获得到的参数 + // Parse the obtained parameters let opt = config::Opt::parse(); - //设置 trace + // 设置 trace // setup_tracing(); let config = load_config(Some(opt.clone().obs_config)); // Initialize Observability - let (_logger, _guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); + let (_logger, guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); - //运行参数 + // Pack and store the guard + GLOBAL_GUARD.set(TracingGuard(Box::new(guard))).unwrap_or_else(|_| { + error!("Unable to set global tracing guard"); + }); + + // Run parameters run(opt) } #[tokio::main] async fn run(opt: config::Opt) -> Result<()> { debug!("opt: {:?}", &opt); - // let config = load_config(Some(opt.clone().obs_config)); - // Initialize Observability - // let (_logger, _guard) = init_obs(config).await; let mut server_addr = net::check_local_server_addr(opt.address.as_str()).unwrap(); From 28a4a917d4b4bfed2bf042db977b15f107e2d56e Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 19 Mar 2025 22:34:26 +0800 Subject: [PATCH 14/27] replace log to tracing --- Cargo.lock | 3 +-- ecstore/src/endpoints.rs | 7 +++--- ecstore/src/global.rs | 2 +- iam/src/lib.rs | 3 ++- packages/obs/Cargo.toml | 14 +++++------ packages/obs/examples/config.toml | 4 +-- packages/obs/src/telemetry.rs | 2 +- rustfs/Cargo.toml | 5 ++-- rustfs/src/main.rs | 41 ++++++++++++++++++++++--------- rustfs/src/storage/ecfs.rs | 4 +-- 10 files changed, 52 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef3176efb..f833d5ceb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5617,7 +5617,6 @@ dependencies = [ "flatbuffers", "futures", "futures-util", - "h2", "http", "http-body", "hyper", @@ -5627,7 +5626,6 @@ dependencies = [ "lazy_static", "local-ip-address", "lock", - "log", "madmin", "matchit 0.8.6", "mime", @@ -5658,6 +5656,7 @@ dependencies = [ "tower 0.5.2", "tower-http", "tracing", + "tracing-core", "tracing-error", "tracing-subscriber", "transform-stream", diff --git a/ecstore/src/endpoints.rs b/ecstore/src/endpoints.rs index 8dbee9928..3bca94804 100644 --- a/ecstore/src/endpoints.rs +++ b/ecstore/src/endpoints.rs @@ -1,4 +1,4 @@ -use tracing::warn; +use tracing::{instrument, warn}; use crate::{ disk::endpoint::{Endpoint, EndpointType}, @@ -407,7 +407,7 @@ pub struct PoolEndpoints { pub platform: String, } -/// list of list of endpoints +/// list of endpoints #[derive(Debug, Clone, Default)] pub struct EndpointServerPools(pub Vec); @@ -532,6 +532,8 @@ impl EndpointServerPools { nodes } + + #[instrument] pub fn hosts_sorted(&self) -> Vec> { let (mut peers, local) = self.peers(); @@ -604,7 +606,6 @@ impl EndpointServerPools { #[cfg(test)] mod test { - use super::*; use std::path::Path; diff --git a/ecstore/src/global.rs b/ecstore/src/global.rs index 8735249c9..f6fc4bdb8 100644 --- a/ecstore/src/global.rs +++ b/ecstore/src/global.rs @@ -61,7 +61,7 @@ pub fn get_global_deployment_id() -> Option { pub fn set_global_endpoints(eps: Vec) { GLOBAL_Endpoints .set(EndpointServerPools::from(eps)) - .expect("GLOBAL_Endpoints set faild") + .expect("GLOBAL_Endpoints set failed") } pub fn get_global_endpoints() -> EndpointServerPools { diff --git a/iam/src/lib.rs b/iam/src/lib.rs index 65484724c..364627a29 100644 --- a/iam/src/lib.rs +++ b/iam/src/lib.rs @@ -2,11 +2,11 @@ use auth::Credentials; use ecstore::error::{Error, Result}; use ecstore::store::ECStore; use error::Error as IamError; -use log::debug; use manager::IamCache; use std::sync::{Arc, OnceLock}; use store::object::ObjectStore; use sys::IamSys; +use tracing::{debug, instrument}; pub mod cache; mod format; @@ -58,6 +58,7 @@ pub fn get_global_action_cred() -> Option { GLOBAL_ACTIVE_CRED.get().cloned() } +#[instrument] pub async fn init_iam_sys(ecstore: Arc) -> Result<()> { debug!("init iam system"); let s = IamCache::new(ObjectStore::new(ecstore)).await; diff --git a/packages/obs/Cargo.toml b/packages/obs/Cargo.toml index 86a9fb6c4..af3e3480d 100644 --- a/packages/obs/Cargo.toml +++ b/packages/obs/Cargo.toml @@ -23,14 +23,14 @@ opentelemetry = { workspace = true } opentelemetry-appender-tracing = { workspace = true, features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } opentelemetry-stdout = { workspace = true } -opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "metrics"] } +opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "gzip-tonic"] } opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } serde = { workspace = true } -tracing = { workspace = true } +tracing = { workspace = true, features = ["std", "attributes"] } tracing-core = { workspace = true } tracing-error = { workspace = true } tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } +tracing-subscriber = { workspace = true, features = ["registry", "std", "fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread"] } rdkafka = { workspace = true, features = ["tokio"], optional = true } reqwest = { workspace = true, optional = true, default-features = false } @@ -41,10 +41,10 @@ local-ip-address = { workspace = true } [dev-dependencies] chrono = { workspace = true } -opentelemetry = { workspace = true, features = ["trace", "metrics"] } -opentelemetry_sdk = { workspace = true, features = ["trace", "rt-tokio"] } -opentelemetry-stdout = { workspace = true, features = ["trace", "metrics"] } -opentelemetry-otlp = { workspace = true, features = ["metrics", "grpc-tonic"] } +opentelemetry = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } +opentelemetry-stdout = { workspace = true } +opentelemetry-otlp = { workspace = true, features = ["grpc-tonic"] } opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true, features = ["std", "attributes"] } diff --git a/packages/obs/examples/config.toml b/packages/obs/examples/config.toml index 2ceac4397..135dd39ac 100644 --- a/packages/obs/examples/config.toml +++ b/packages/obs/examples/config.toml @@ -1,9 +1,9 @@ [observability] endpoint = "http://localhost:4317" use_stdout = true -sample_ratio = 0.5 +sample_ratio = 1 meter_interval = 30 -service_name = "rustfs_obs_service" +service_name = "rustfs_obs" service_version = "0.1.0" deployment_environment = "develop" diff --git a/packages/obs/src/telemetry.rs b/packages/obs/src/telemetry.rs index cafe5badc..914071903 100644 --- a/packages/obs/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -129,7 +129,7 @@ fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { let tracer_provider = if config.endpoint.is_empty() { builder - .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + .with_batch_exporter(opentelemetry_stdout::SpanExporter::default()) .build() } else { let exporter = opentelemetry_otlp::SpanExporter::builder() diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 530cc57ae..640baea76 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -16,7 +16,7 @@ workspace = true [dependencies] madmin.workspace = true -log.workspace = true +#log.workspace = true async-trait.workspace = true bytes.workspace = true clap.workspace = true @@ -25,7 +25,7 @@ ecstore.workspace = true flatbuffers.workspace = true futures.workspace = true futures-util.workspace = true -h2 = "0.4.7" +#h2 = "0.4.7" hyper.workspace = true hyper-util.workspace = true http.workspace = true @@ -57,6 +57,7 @@ tokio-stream.workspace = true tonic = { version = "0.12.3", features = ["gzip"] } tonic-reflection.workspace = true tower.workspace = true +tracing-core = { workspace = true } tracing-error.workspace = true tracing-subscriber.workspace = true transform-stream.workspace = true diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 6f4fee1ee..787ee8c99 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -36,14 +36,16 @@ use hyper_util::{ use iam::init_iam_sys; use once_cell::sync::OnceCell; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; -use rustfs_obs::{init_obs, load_config}; +use rustfs_obs::{init_obs, load_config, BaseLogEntry, ServerLogEntry}; use s3s::{host::MultiDomain, service::S3ServiceBuilder}; use service::hybrid; +use std::time::SystemTime; use std::{io::IsTerminal, net::SocketAddr}; use tokio::net::TcpListener; use tonic::{metadata::MetadataValue, Request, Status}; use tower_http::cors::CorsLayer; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, info_span, warn}; +use tracing_core::Level; use tracing_error::ErrorLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -101,28 +103,43 @@ fn print_server_info() { info!("Docs: {}", cfg.doc()); } -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { // Parse the obtained parameters let opt = config::Opt::parse(); - + println!("config: {:?}", &opt); // 设置 trace // setup_tracing(); let config = load_config(Some(opt.clone().obs_config)); // Initialize Observability - let (_logger, guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); - + let (logger, guard) = init_obs(config).await; + // let (logger, guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); + // let _rr = tokio::runtime::Runtime::new()?.block_on(async { + let start_time = SystemTime::now(); + let base_entry = BaseLogEntry::new() + .timestamp(chrono::DateTime::from(start_time)) + .message(Some("main init obs end".to_string())) + .request_id(Some("main".to_string())); + let server_entry = ServerLogEntry::new(Level::INFO, "main_server_entry".to_string()) + .with_base(base_entry) + .user_id(Some("user_id".to_string())); + let _r = logger.lock().await.log_server_entry(server_entry).await; + // }); // Pack and store the guard GLOBAL_GUARD.set(TracingGuard(Box::new(guard))).unwrap_or_else(|_| { error!("Unable to set global tracing guard"); }); // Run parameters - run(opt) + run(opt).await } - -#[tokio::main] +// +// #[tokio::main] async fn run(opt: config::Opt) -> Result<()> { + let span = info_span!("trace-main-run"); + let _enter = span.enter(); + debug!("opt: {:?}", &opt); let mut server_addr = net::check_local_server_addr(opt.address.as_str()).unwrap(); @@ -259,7 +276,7 @@ async fn run(opt: config::Opt) -> Result<()> { match res { Ok(conn) => conn, Err(err) => { - tracing::error!("error accepting connection: {err}"); + error!("error accepting connection: {err}"); continue; } } @@ -278,10 +295,10 @@ async fn run(opt: config::Opt) -> Result<()> { tokio::select! { () = graceful.shutdown() => { - tracing::debug!("Gracefully shutdown!"); + debug!("Gracefully shutdown!"); }, () = tokio::time::sleep(std::time::Duration::from_secs(10)) => { - tracing::debug!("Waited 10 seconds for graceful shutdown, aborting..."); + debug!("Waited 10 seconds for graceful shutdown, aborting..."); } } }); diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index edb936dc3..7dfcb46c1 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -42,7 +42,6 @@ use http::HeaderMap; use iam::policy::action::Action; use iam::policy::action::S3Action; use lazy_static::lazy_static; -use log::warn; use s3s::dto::*; use s3s::s3_error; use s3s::S3Error; @@ -57,6 +56,7 @@ use tokio_util::io::StreamReader; use tracing::debug; use tracing::error; use tracing::info; +use tracing::warn; use transform_stream::AsyncTryStream; use uuid::Uuid; @@ -217,7 +217,7 @@ impl S3 for FS { #[tracing::instrument(level = "debug", skip(self, req))] async fn delete_bucket(&self, req: S3Request) -> S3Result> { let input = req.input; - // TODO: DeleteBucketInput 没有force参数? + // TODO: DeleteBucketInput 没有 force 参数? let Some(store) = new_object_layer_fn() else { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; From aeb696687b07e9616c581cb77d2ed47ba9bc6bd2 Mon Sep 17 00:00:00 2001 From: houseme Date: Sat, 22 Mar 2025 21:30:10 +0800 Subject: [PATCH 15/27] update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 83b9ef43c..7017949a1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ .devcontainer rustfs/static/* vendor -cli/rustfs-gui/embedded-rustfs/rustfs \ No newline at end of file +cli/rustfs-gui/embedded-rustfs/rustfs +config/obs.toml \ No newline at end of file From 2b57af75ea8067fce5b469dbf320469a1b42242b Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 25 Mar 2025 19:02:52 +0800 Subject: [PATCH 16/27] upgrade opentelemetry create from 0.28.0 to 0.29.0 --- .docker/observability/jaeger-config.yaml | 9 ++-- .../observability/otel-collector-config.yaml | 34 +++++++-------- Cargo.lock | 43 ++++++++----------- Cargo.toml | 14 +++--- config/obs.example.toml | 2 +- packages/obs/examples/server.rs | 9 ++-- rustfs/src/main.rs | 22 +++++----- 7 files changed, 65 insertions(+), 68 deletions(-) diff --git a/.docker/observability/jaeger-config.yaml b/.docker/observability/jaeger-config.yaml index 31b5a9d6d..0df35f836 100644 --- a/.docker/observability/jaeger-config.yaml +++ b/.docker/observability/jaeger-config.yaml @@ -1,11 +1,10 @@ - service: - extensions: [jaeger_storage, jaeger_query, remote_sampling, healthcheckv2] + extensions: [ jaeger_storage, jaeger_query, remote_sampling, healthcheckv2 ] pipelines: traces: - receivers: [otlp, jaeger, zipkin] - processors: [batch, adaptive_sampling] - exporters: [jaeger_storage_exporter] + receivers: [ otlp, jaeger, zipkin ] + processors: [ batch, adaptive_sampling ] + exporters: [ jaeger_storage_exporter ] telemetry: resource: service.name: jaeger diff --git a/.docker/observability/otel-collector-config.yaml b/.docker/observability/otel-collector-config.yaml index 9e27c4e22..24d67dc3c 100644 --- a/.docker/observability/otel-collector-config.yaml +++ b/.docker/observability/otel-collector-config.yaml @@ -1,13 +1,13 @@ receivers: otlp: protocols: - grpc: # OTLP gRPC 接收器 + grpc: # OTLP gRPC 接收器 endpoint: 0.0.0.0:4317 - http: # OTLP HTTP 接收器 + http: # OTLP HTTP 接收器 endpoint: 0.0.0.0:4318 processors: - batch: # 批处理处理器,提升吞吐量 + batch: # 批处理处理器,提升吞吐量 timeout: 5s send_batch_size: 1000 memory_limiter: @@ -15,16 +15,16 @@ processors: limit_mib: 512 exporters: - otlp/traces: # OTLP 导出器,用于跟踪数据 + otlp/traces: # OTLP 导出器,用于跟踪数据 endpoint: "jaeger:4317" # Jaeger 的 OTLP gRPC 端点 tls: insecure: true # 开发环境禁用 TLS,生产环境需配置证书 - prometheus: # Prometheus 导出器,用于指标数据 + prometheus: # Prometheus 导出器,用于指标数据 endpoint: "0.0.0.0:8889" # Prometheus 刮取端点 - namespace: "otel" # 指标前缀 + namespace: "rustfs" # 指标前缀 send_timestamps: true # 发送时间戳 # enable_open_metrics: true - loki: # Loki 导出器,用于日志数据 + loki: # Loki 导出器,用于日志数据 # endpoint: "http://loki:3100/otlp/v1/logs" endpoint: "http://loki:3100/loki/api/v1/push" tls: @@ -34,20 +34,20 @@ extensions: pprof: zpages: service: - extensions: [health_check, pprof, zpages] # 启用扩展 + extensions: [ health_check, pprof, zpages ] # 启用扩展 pipelines: traces: - receivers: [otlp] - processors: [memory_limiter,batch] - exporters: [otlp/traces] + receivers: [ otlp ] + processors: [ memory_limiter,batch ] + exporters: [ otlp/traces ] metrics: - receivers: [otlp] - processors: [batch] - exporters: [prometheus] + receivers: [ otlp ] + processors: [ batch ] + exporters: [ prometheus ] logs: - receivers: [otlp] - processors: [batch] - exporters: [loki] + receivers: [ otlp ] + processors: [ batch ] + exporters: [ loki ] telemetry: logs: level: "info" # Collector 日志级别 diff --git a/Cargo.lock b/Cargo.lock index f833d5ceb..460ba8e12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4273,9 +4273,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "opentelemetry" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" +checksum = "768ee97dc5cd695a4dd4a69a0678fb42789666b5a89e8c0af48bb06c6e427120" dependencies = [ "futures-core", "futures-sink", @@ -4287,9 +4287,9 @@ dependencies = [ [[package]] name = "opentelemetry-appender-tracing" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c513c7af3bec30113f3d4620134ff923295f1e9c580fda2b8abe0831f925ddc0" +checksum = "e716f864eb23007bdd9dc4aec381e188a1cee28eecf22066772b5fd822b9727d" dependencies = [ "opentelemetry", "tracing", @@ -4301,9 +4301,9 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" +checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" dependencies = [ "async-trait", "bytes", @@ -4315,11 +4315,10 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" +checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" dependencies = [ - "async-trait", "futures-core", "http", "opentelemetry", @@ -4336,9 +4335,9 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" +checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -4348,39 +4347,35 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb3a2f78c2d55362cd6c313b8abedfbc0142ab3c2676822068fd2ab7d51f9b7" +checksum = "84b29a9f89f1a954936d5aa92f19b2feec3c8f3971d3e96206640db7f9706ae3" [[package]] name = "opentelemetry-stdout" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb0e5a5132e4b80bf037a78e3e12c8402535199f5de490d0c38f7eac71bc831" +checksum = "a7e27d446dabd68610ef0b77d07b102ecde827a4596ea9c01a4d3811e945b286" dependencies = [ - "async-trait", "chrono", "futures-util", "opentelemetry", "opentelemetry_sdk", - "serde", - "thiserror 2.0.12", ] [[package]] name = "opentelemetry_sdk" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" dependencies = [ - "async-trait", "futures-channel", "futures-executor", "futures-util", "glob", "opentelemetry", "percent-encoding", - "rand 0.8.5", + "rand 0.9.0", "serde_json", "thiserror 2.0.12", "tokio", @@ -7032,9 +7027,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" +checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" dependencies = [ "js-sys", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index e45251674..150a0e937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,12 +61,12 @@ local-ip-address = "0.6.3" mime = "0.3.17" netif = "0.1.6" once_cell = "1.21.1" -opentelemetry = { version = "0.28" } -opentelemetry-appender-tracing = { version = "0.28.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } -opentelemetry_sdk = { version = "0.28" } -opentelemetry-stdout = { version = "0.28.0" } -opentelemetry-otlp = { version = "0.28" } -opentelemetry-semantic-conventions = { version = "0.28.0", features = ["semconv_experimental"] } +opentelemetry = { version = "0.29" } +opentelemetry-appender-tracing = { version = "0.29.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes"] } +opentelemetry_sdk = { version = "0.29" } +opentelemetry-stdout = { version = "0.29.0" } +opentelemetry-otlp = { version = "0.29" } +opentelemetry-semantic-conventions = { version = "0.29.0", features = ["semconv_experimental"] } pin-project-lite = "0.2" # pin-utils = "0.1.0" prost = "0.13.4" @@ -110,7 +110,7 @@ tracing-core = "0.1.33" tracing-error = "0.2.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] } tracing-appender = "0.2.3" -tracing-opentelemetry = "0.29" +tracing-opentelemetry = "0.30" transform-stream = "0.3.1" url = "2.5.4" uuid = { version = "1.15.1", features = [ diff --git a/config/obs.example.toml b/config/obs.example.toml index 59bd0f344..50d4ae757 100644 --- a/config/obs.example.toml +++ b/config/obs.example.toml @@ -3,7 +3,7 @@ endpoint = "" # Default is "http://localhost:4317" if not specified use_stdout = false sample_ratio = 0.5 meter_interval = 30 -service_name = "rustfs_obs_service" +service_name = "rustfs" service_version = "0.1.0" deployment_environment = "develop" diff --git a/packages/obs/examples/server.rs b/packages/obs/examples/server.rs index 15a64c8be..0af2fcd02 100644 --- a/packages/obs/examples/server.rs +++ b/packages/obs/examples/server.rs @@ -10,6 +10,9 @@ async fn main() { let obs_conf = Some("packages/obs/examples/config.toml".to_string()); let config = load_config(obs_conf); let (_logger, _guard) = init_obs(config.clone()).await; + let span = tracing::span!(Level::INFO, "main"); + let _enter = span.enter(); + info!("Program starts"); // Simulate the operation tokio::time::sleep(Duration::from_millis(100)).await; run( @@ -28,7 +31,7 @@ async fn run(bucket: String, object: String, user: String, service_name: String) info!("Log module initialization is completed service_name: {:?}", service_name); // Record Metrics - let meter = global::meter("rustfs.rs"); + let meter = global::meter("rustfs"); let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); request_duration.record( start_time.elapsed().unwrap().as_secs_f64(), @@ -59,9 +62,9 @@ async fn run(bucket: String, object: String, user: String, service_name: String) #[instrument(fields(bucket, object, user))] async fn put_object(bucket: String, object: String, user: String) { let start_time = SystemTime::now(); - info!("Starting put_object operation"); + info!("Starting put_object operation time: {:?}", start_time); - let meter = global::meter("rustfs.rs"); + let meter = global::meter("rustfs"); let request_duration = meter.f64_histogram("s3_request_duration_seconds").build(); request_duration.record( start_time.elapsed().unwrap().as_secs_f64(), diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 787ee8c99..08e10859b 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -107,29 +107,29 @@ fn print_server_info() { async fn main() -> Result<()> { // Parse the obtained parameters let opt = config::Opt::parse(); - println!("config: {:?}", &opt); - // 设置 trace - // setup_tracing(); + // println!("config: {:?}", &opt); + // Load the configuration file let config = load_config(Some(opt.clone().obs_config)); + // Initialize Observability let (logger, guard) = init_obs(config).await; - // let (logger, guard) = tokio::runtime::Runtime::new()?.block_on(async { init_obs(config).await }); - // let _rr = tokio::runtime::Runtime::new()?.block_on(async { + + // Pack and store the guard + GLOBAL_GUARD.set(TracingGuard(Box::new(guard))).unwrap_or_else(|_| { + error!("Unable to set global tracing guard"); + }); + + // Initialize the logger let start_time = SystemTime::now(); let base_entry = BaseLogEntry::new() .timestamp(chrono::DateTime::from(start_time)) - .message(Some("main init obs end".to_string())) + .message(Some("main init obs start".to_string())) .request_id(Some("main".to_string())); let server_entry = ServerLogEntry::new(Level::INFO, "main_server_entry".to_string()) .with_base(base_entry) .user_id(Some("user_id".to_string())); let _r = logger.lock().await.log_server_entry(server_entry).await; - // }); - // Pack and store the guard - GLOBAL_GUARD.set(TracingGuard(Box::new(guard))).unwrap_or_else(|_| { - error!("Unable to set global tracing guard"); - }); // Run parameters run(opt).await From 23ead0ea999730d9cad28a2f498fd4471da23fec Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 26 Mar 2025 15:18:23 +0800 Subject: [PATCH 17/27] add prometheus --- Cargo.lock | 41 +++++++++++++++++++++++++++++++++-- Cargo.toml | 2 ++ packages/obs/Cargo.toml | 2 ++ packages/obs/src/telemetry.rs | 22 +++++++++++++++---- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 460ba8e12..213aeb0ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4333,6 +4333,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-prometheus" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac8c4fc7bd450bcb5b1cbc7325755e86d9f82f1fd80ad8b3441887b715f6a2d" +dependencies = [ + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "prometheus", + "protobuf 2.28.0", + "tracing", +] + [[package]] name = "opentelemetry-proto" version = "0.29.0" @@ -4961,6 +4975,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.3", + "protobuf 2.28.0", + "thiserror 1.0.69", +] + [[package]] name = "prost" version = "0.13.5" @@ -5013,6 +5042,12 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "protobuf" version = "3.7.2" @@ -5041,7 +5076,7 @@ dependencies = [ "flatbuffers", "prost", "prost-build", - "protobuf", + "protobuf 3.7.2", "tokio", "tonic", "tonic-build", @@ -5631,7 +5666,7 @@ dependencies = [ "prost", "prost-build", "prost-types", - "protobuf", + "protobuf 3.7.2", "protos", "rmp-serde", "rust-embed", @@ -5691,9 +5726,11 @@ dependencies = [ "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-otlp", + "opentelemetry-prometheus", "opentelemetry-semantic-conventions", "opentelemetry-stdout", "opentelemetry_sdk", + "prometheus", "rdkafka", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index 150a0e937..4977bc9d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,8 +66,10 @@ opentelemetry-appender-tracing = { version = "0.29.1", features = ["experimental opentelemetry_sdk = { version = "0.29" } opentelemetry-stdout = { version = "0.29.0" } opentelemetry-otlp = { version = "0.29" } +opentelemetry-prometheus = { version = "0.29" } opentelemetry-semantic-conventions = { version = "0.29.0", features = ["semconv_experimental"] } pin-project-lite = "0.2" +prometheus = "0.13.4" # pin-utils = "0.1.0" prost = "0.13.4" prost-build = "0.13.4" diff --git a/packages/obs/Cargo.toml b/packages/obs/Cargo.toml index af3e3480d..4290b76b3 100644 --- a/packages/obs/Cargo.toml +++ b/packages/obs/Cargo.toml @@ -24,7 +24,9 @@ opentelemetry-appender-tracing = { workspace = true, features = ["experimental_u opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } opentelemetry-stdout = { workspace = true } opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "gzip-tonic"] } +opentelemetry-prometheus = { workspace = true } opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } +prometheus = { workspace = true } serde = { workspace = true } tracing = { workspace = true, features = ["std", "attributes"] } tracing-core = { workspace = true } diff --git a/packages/obs/src/telemetry.rs b/packages/obs/src/telemetry.rs index 914071903..7d931b922 100644 --- a/packages/obs/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -13,6 +13,7 @@ use opentelemetry_semantic_conventions::{ attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_NAME, SERVICE_VERSION}, SCHEMA_URL, }; +use prometheus::Registry; use std::io::IsTerminal; use tracing_error::ErrorLayer; use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; @@ -45,6 +46,13 @@ pub struct OtelGuard { tracer_provider: SdkTracerProvider, meter_provider: SdkMeterProvider, logger_provider: SdkLoggerProvider, + registry: Registry, +} + +impl OtelGuard { + pub fn get_registry(&self) -> &Registry { + &self.registry + } } impl Drop for OtelGuard { @@ -78,7 +86,7 @@ fn resource(config: &OtelConfig) -> Resource { } /// Initialize Meter Provider -fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { +fn init_meter_provider(config: &OtelConfig) -> (SdkMeterProvider, Registry) { let mut builder = MeterProviderBuilder::default().with_resource(resource(config)); // If endpoint is empty, use stdout output if config.endpoint.is_empty() { @@ -109,10 +117,15 @@ fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { ); } } + let registry = Registry::new(); + let prometheus_exporter = opentelemetry_prometheus::exporter() + .with_registry(registry.clone()) + .build() + .unwrap(); - let meter_provider = builder.build(); + let meter_provider = builder.with_reader(prometheus_exporter).build(); global::set_meter_provider(meter_provider.clone()); - meter_provider + (meter_provider, registry) } /// Initialize Tracer Provider @@ -154,7 +167,7 @@ fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { /// Initialize Telemetry pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { let tracer_provider = init_tracer_provider(config); - let meter_provider = init_meter_provider(config); + let (meter_provider, prometheus_registry) = init_meter_provider(config); let tracer = tracer_provider.tracer(config.service_name.clone()); // Initialize logger provider based on configuration @@ -229,5 +242,6 @@ pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { tracer_provider, meter_provider, logger_provider, + registry: prometheus_registry, } } From 64d87faaf3f5a287b194f784196f7a0c4a89f60d Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 26 Mar 2025 16:59:34 +0800 Subject: [PATCH 18/27] feat: add metrics_handler --- packages/obs/src/lib.rs | 3 +- packages/obs/src/telemetry.rs | 82 +++++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index 0db02853f..576edf93e 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -50,7 +50,7 @@ pub use logger::{ pub use logger::{LogError, Logger}; pub use sink::Sink; use std::sync::Arc; -pub use telemetry::init_telemetry; +pub use telemetry::{get_global_registry, init_telemetry, metrics_handler}; use tokio::sync::Mutex; pub use utils::{get_local_ip, get_local_ip_with_default}; pub use worker::start_worker; @@ -86,6 +86,7 @@ pub async fn init_obs(config: AppConfig) -> (Arc>, telemetry::Otel /// # Example /// ``` /// use rustfs_obs::get_logger; +/// /// let logger = get_logger(); /// ``` pub fn get_logger() -> &'static Arc> { diff --git a/packages/obs/src/telemetry.rs b/packages/obs/src/telemetry.rs index 7d931b922..8df3bbf98 100644 --- a/packages/obs/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -13,8 +13,11 @@ use opentelemetry_semantic_conventions::{ attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_NAME, SERVICE_VERSION}, SCHEMA_URL, }; -use prometheus::Registry; +use prometheus::{Encoder, Registry, TextEncoder}; use std::io::IsTerminal; +use std::sync::Arc; +use tokio::sync::{Mutex, OnceCell}; +use tracing::{info, warn}; use tracing_error::ErrorLayer; use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; @@ -46,13 +49,6 @@ pub struct OtelGuard { tracer_provider: SdkTracerProvider, meter_provider: SdkMeterProvider, logger_provider: SdkLoggerProvider, - registry: Registry, -} - -impl OtelGuard { - pub fn get_registry(&self) -> &Registry { - &self.registry - } } impl Drop for OtelGuard { @@ -69,6 +65,57 @@ impl Drop for OtelGuard { } } +/// Global registry for Prometheus metrics +static GLOBAL_REGISTRY: OnceCell>> = OnceCell::const_new(); + +/// Get the global registry instance +/// This function returns a reference to the global registry instance. +/// +/// # Returns +/// A reference to the global registry instance +/// +/// # Example +/// ``` +/// use rustfs_obs::get_global_registry; +/// +/// let registry = get_global_registry(); +/// ``` +pub fn get_global_registry() -> Arc> { + GLOBAL_REGISTRY.get().unwrap().clone() +} + +/// Prometheus metric endpoints +/// This function returns a string containing the Prometheus metrics. +/// The metrics are collected from the global registry. +/// The function is used to expose the metrics via an HTTP endpoint. +/// +/// # Returns +/// A string containing the Prometheus metrics +/// +/// # Example +/// ``` +/// use rustfs_obs::metrics_handler; +/// +/// async fn main() { +/// let metrics = metrics_handler().await; +/// println!("{}", metrics); +/// } +/// ``` +pub async fn metrics_handler() -> String { + let encoder = TextEncoder::new(); + // Get a reference to the registry for reading metrics + let registry = get_global_registry().lock().await.to_owned(); + let metric_families = registry.gather(); + if metric_families.is_empty() { + warn!("No metrics available in Prometheus registry"); + } else { + info!("Metrics collected: {} families", metric_families.len()); + } + let mut buffer = Vec::new(); + encoder.encode(&metric_families, &mut buffer).unwrap(); + String::from_utf8(buffer).unwrap_or_else(|_| "Error encoding metrics".to_string()) +} + /// create OpenTelemetry Resource fn resource(config: &OtelConfig) -> Resource { Resource::builder() @@ -86,7 +133,7 @@ fn resource(config: &OtelConfig) -> Resource { } /// Initialize Meter Provider -fn init_meter_provider(config: &OtelConfig) -> (SdkMeterProvider, Registry) { +fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { let mut builder = MeterProviderBuilder::default().with_resource(resource(config)); // If endpoint is empty, use stdout output if config.endpoint.is_empty() { @@ -118,14 +165,14 @@ fn init_meter_provider(config: &OtelConfig) -> (SdkMeterProvider, Registry) { } } let registry = Registry::new(); - let prometheus_exporter = opentelemetry_prometheus::exporter() - .with_registry(registry.clone()) - .build() - .unwrap(); - + // Set global registry + GLOBAL_REGISTRY.set(Arc::new(Mutex::new(registry.clone()))).unwrap(); + // Create Prometheus exporter + let prometheus_exporter = opentelemetry_prometheus::exporter().with_registry(registry).build().unwrap(); + // Build meter provider let meter_provider = builder.with_reader(prometheus_exporter).build(); global::set_meter_provider(meter_provider.clone()); - (meter_provider, registry) + meter_provider } /// Initialize Tracer Provider @@ -167,7 +214,7 @@ fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { /// Initialize Telemetry pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { let tracer_provider = init_tracer_provider(config); - let (meter_provider, prometheus_registry) = init_meter_provider(config); + let meter_provider = init_meter_provider(config); let tracer = tracer_provider.tracer(config.service_name.clone()); // Initialize logger provider based on configuration @@ -235,13 +282,12 @@ pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { registry.with(ErrorLayer::default()).with(fmt_layer).init(); if !config.endpoint.is_empty() { - tracing::info!("OpenTelemetry telemetry initialized with OTLP endpoint: {}", config.endpoint); + info!("OpenTelemetry telemetry initialized with OTLP endpoint: {}", config.endpoint); } OtelGuard { tracer_provider, meter_provider, logger_provider, - registry: prometheus_registry, } } From 6f706c102e04683d5bcdaef69930453b6ba75634 Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 26 Mar 2025 17:26:34 +0800 Subject: [PATCH 19/27] merge main --- rustfs/src/storage/ecfs.rs | 83 ++++++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 92f32769f..345493059 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -4,10 +4,6 @@ use super::options::extract_metadata; use super::options::put_opts; use crate::auth::get_condition_values; use crate::storage::access::ReqInfo; -use crate::storage::error::to_s3_error; -use crate::storage::options::copy_dst_opts; -use crate::storage::options::copy_src_opts; -use crate::storage::options::{extract_metadata_from_mime, get_opts}; use bytes::Bytes; use common::error::Result; use ecstore::bucket::error::BucketMetadataError; @@ -44,7 +40,12 @@ use futures::pin_mut; use futures::{Stream, StreamExt}; use http::HeaderMap; use lazy_static::lazy_static; +use policy::auth; +use policy::policy::action::Action; +use policy::policy::action::S3Action; use policy::policy::BucketPolicy; +use policy::policy::BucketPolicyArgs; +use policy::policy::Validator; use s3s::dto::*; use s3s::s3_error; use s3s::S3Error; @@ -56,10 +57,18 @@ use std::fmt::Debug; use std::str::FromStr; use tokio_util::io::ReaderStream; use tokio_util::io::StreamReader; -use tracing::{debug, error, info, warn}; +use tracing::debug; +use tracing::error; +use tracing::info; +use tracing::warn; use transform_stream::AsyncTryStream; use uuid::Uuid; +use crate::storage::error::to_s3_error; +use crate::storage::options::copy_dst_opts; +use crate::storage::options::copy_src_opts; +use crate::storage::options::{extract_metadata_from_mime, get_opts}; + macro_rules! try_ { ($result:expr) => { match $result { @@ -554,7 +563,7 @@ impl S3 for FS { let mut req = req; - if authorize_request(&mut req, vec![Action::S3Action(S3Action::ListAllMyBucketsAction)]) + if authorize_request(&mut req, Action::S3Action(S3Action::ListAllMyBucketsAction)) .await .is_err() { @@ -564,10 +573,10 @@ impl S3 for FS { req_info.bucket = Some(info.name.clone()); futures::executor::block_on(async { - authorize_request(&mut req, vec![Action::S3Action(S3Action::ListBucketAction)]) + authorize_request(&mut req, Action::S3Action(S3Action::ListBucketAction)) .await .is_ok() - || authorize_request(&mut req, vec![Action::S3Action(S3Action::GetBucketLocationAction)]) + || authorize_request(&mut req, Action::S3Action(S3Action::GetBucketLocationAction)) .await .is_ok() }) @@ -1214,9 +1223,52 @@ impl S3 for FS { async fn get_bucket_policy_status( &self, - _req: S3Request, + req: S3Request, ) -> S3Result> { - Err(s3_error!(NotImplemented, "GetBucketPolicyStatus is not implemented yet")) + let GetBucketPolicyStatusInput { bucket, .. } = req.input; + + let Some(store) = new_object_layer_fn() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + + store + .get_bucket_info(&bucket, &BucketOptions::default()) + .await + .map_err(to_s3_error)?; + + let conditions = get_condition_values(&req.headers, &auth::Credentials::default()); + + let read_olny = PolicySys::is_allowed(&BucketPolicyArgs { + bucket: &bucket, + action: Action::S3Action(S3Action::ListBucketAction), + is_owner: false, + account: "", + groups: &None, + conditions: &conditions, + object: "", + }) + .await; + + let write_olny = PolicySys::is_allowed(&BucketPolicyArgs { + bucket: &bucket, + action: Action::S3Action(S3Action::PutObjectAction), + is_owner: false, + account: "", + groups: &None, + conditions: &conditions, + object: "", + }) + .await; + + let is_public = read_olny && write_olny; + + let output = GetBucketPolicyStatusOutput { + policy_status: Some(PolicyStatus { + is_public: Some(is_public), + }), + }; + + Ok(S3Response::new(output)) } async fn get_bucket_policy(&self, req: S3Request) -> S3Result> { @@ -1260,18 +1312,17 @@ impl S3 for FS { // warn!("input policy {}", &policy); - let cfg = BucketPolicy::unmarshal(policy.as_bytes()).map_err(to_s3_error)?; + let cfg: BucketPolicy = + serde_json::from_str(&policy).map_err(|e| s3_error!(InvalidArgument, "parse policy faild {:?}", e))?; - // warn!("parse policy {:?}", &cfg); - - if let Err(err) = cfg.validate(&bucket) { + if let Err(err) = cfg.is_valid() { warn!("put_bucket_policy err input {:?}, {:?}", &policy, err); return Err(s3_error!(InvalidPolicyDocument)); } - let data = cfg.marshal_msg().map_err(to_s3_error)?; + let data = serde_json::to_vec(&cfg).map_err(|e| s3_error!(InternalError, "parse policy faild {:?}", e))?; - metadata_sys::update(&bucket, BUCKET_POLICY_CONFIG, data.into()) + metadata_sys::update(&bucket, BUCKET_POLICY_CONFIG, data) .await .map_err(to_s3_error)?; From c4c6d439bcfabb581cd619da9c131c9e60603195 Mon Sep 17 00:00:00 2001 From: houseme Date: Thu, 27 Mar 2025 18:03:47 +0800 Subject: [PATCH 20/27] feat(obs): enhance OpenTelemetry configuration and logging Improve observability setup with the following changes: - Replace static OnceCell with tokio::sync::OnceCell for guard management - Add logger_level to OtelConfig for configurable tracing verbosity - Improve telemetry initialization with better error handling - Enhance logging filters and span configuration Breaking Changes: - Configuration now requires logger_level field - Global guard management uses async-safe primitives Example config: observability: endpoint: "http://localhost:4317" logger_level: "debug" # New required field --- config/obs.example.toml | 3 +- packages/obs/src/config.rs | 58 +++++++++++++++---- packages/obs/src/global.rs | 89 ++++++++++++++++++++++++++++ packages/obs/src/lib.rs | 14 ++--- packages/obs/src/logger.rs | 63 +++++++++++++++++++- packages/obs/src/telemetry.rs | 105 +++++++++++++++++++++------------- rustfs/src/main.rs | 47 +++------------ 7 files changed, 281 insertions(+), 98 deletions(-) create mode 100644 packages/obs/src/global.rs diff --git a/config/obs.example.toml b/config/obs.example.toml index 50d4ae757..57f454441 100644 --- a/config/obs.example.toml +++ b/config/obs.example.toml @@ -5,7 +5,8 @@ sample_ratio = 0.5 meter_interval = 30 service_name = "rustfs" service_version = "0.1.0" -deployment_environment = "develop" +environment = "develop" +looger_level = "info" [sinks] [sinks.kafka] # Kafka sink is disabled by default diff --git a/packages/obs/src/config.rs b/packages/obs/src/config.rs index 744c0ec16..3acc2efdb 100644 --- a/packages/obs/src/config.rs +++ b/packages/obs/src/config.rs @@ -1,22 +1,40 @@ +use crate::global::{ENVIRONMENT, LOGGER_LEVEL, METER_INTERVAL, SAMPLE_RATIO, SERVICE_NAME, SERVICE_VERSION}; use config::{Config, File, FileFormat}; use serde::Deserialize; use std::env; /// OpenTelemetry Configuration -/// Add service name, service version, deployment environment +/// Add service name, service version, environment /// Add interval time for metric collection /// Add sample ratio for trace sampling /// Add endpoint for metric collection /// Add use_stdout for output to stdout -#[derive(Debug, Deserialize, Clone, Default)] +/// Add logger level for log level +#[derive(Debug, Deserialize, Clone)] pub struct OtelConfig { pub endpoint: String, - pub use_stdout: bool, - pub sample_ratio: f64, - pub meter_interval: u64, - pub service_name: String, - pub service_version: String, - pub deployment_environment: String, + pub use_stdout: Option, + pub sample_ratio: Option, + pub meter_interval: Option, + pub service_name: Option, + pub service_version: Option, + pub environment: Option, + pub logger_level: Option, +} + +impl Default for OtelConfig { + fn default() -> Self { + OtelConfig { + endpoint: "".to_string(), + use_stdout: Some(true), + sample_ratio: Some(SAMPLE_RATIO), + meter_interval: Some(METER_INTERVAL), + service_name: Some(SERVICE_NAME.to_string()), + service_version: Some(SERVICE_VERSION.to_string()), + environment: Some(ENVIRONMENT.to_string()), + logger_level: Some(LOGGER_LEVEL.to_string()), + } + } } /// Kafka Sink Configuration - Add batch parameters @@ -39,7 +57,7 @@ pub struct WebhookSinkConfig { } /// File Sink Configuration - Add buffering parameters -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] pub struct FileSinkConfig { pub enabled: bool, pub path: String, @@ -48,6 +66,18 @@ pub struct FileSinkConfig { pub flush_threshold: Option, // Refresh threshold, default 100 logs } +impl Default for FileSinkConfig { + fn default() -> Self { + FileSinkConfig { + enabled: true, + path: "logs/app.log".to_string(), + buffer_size: Some(8192), + flush_interval_ms: Some(1000), + flush_threshold: Some(100), + } + } +} + /// Sink configuration collection #[derive(Debug, Deserialize, Clone, Default)] pub struct SinkConfig { @@ -57,11 +87,19 @@ pub struct SinkConfig { } ///Logger Configuration -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] pub struct LoggerConfig { pub queue_capacity: Option, } +impl Default for LoggerConfig { + fn default() -> Self { + LoggerConfig { + queue_capacity: Some(1000), + } + } +} + /// Overall application configuration /// Add observability, sinks, and logger configuration /// diff --git a/packages/obs/src/global.rs b/packages/obs/src/global.rs new file mode 100644 index 000000000..86297b8b4 --- /dev/null +++ b/packages/obs/src/global.rs @@ -0,0 +1,89 @@ +use crate::telemetry::OtelGuard; +use std::sync::{Arc, Mutex}; +use tokio::sync::{OnceCell, SetError}; +use tracing::{error, info}; + +pub(crate) const USE_STDOUT: bool = true; +pub(crate) const SERVICE_NAME: &str = "RustFS"; +pub(crate) const SAMPLE_RATIO: f64 = 1.0; +pub(crate) const METER_INTERVAL: u64 = 60; +pub(crate) const SERVICE_VERSION: &str = "0.1.0"; +pub(crate) const ENVIRONMENT: &str = "development"; +pub(crate) const LOGGER_LEVEL: &str = "info"; + +/// Global guard for OpenTelemetry tracing +static GLOBAL_GUARD: OnceCell>> = OnceCell::const_new(); + +/// Error type for global guard operations +#[derive(Debug, thiserror::Error)] +pub enum GuardError { + #[error("Failed to set global guard: {0}")] + SetError(#[from] SetError>>), + #[error("Global guard not initialized")] + NotInitialized, +} + +/// Set the global guard for OpenTelemetry +/// +/// # Arguments +/// * `guard` - The OtelGuard instance to set globally +/// +/// # Returns +/// * `Ok(())` if successful +/// * `Err(GuardError)` if setting fails +/// +/// # Example +/// ```rust +/// use rustfs_obs::{init_telemetry, load_config, set_global_guard}; +/// +/// async fn init() -> Result<(), Box> { +/// let config = load_config(None); +/// let guard = init_telemetry(&config.observability).await?; +/// set_global_guard(guard)?; +/// Ok(()) +/// } +/// ``` +pub fn set_global_guard(guard: OtelGuard) -> Result<(), GuardError> { + info!("Initializing global OpenTelemetry guard"); + GLOBAL_GUARD.set(Arc::new(Mutex::new(guard))).map_err(GuardError::SetError) +} + +/// Get the global guard for OpenTelemetry +/// +/// # Returns +/// * `Ok(Arc>)` if guard exists +/// * `Err(GuardError)` if guard not initialized +/// +/// # Example +/// ```rust +/// use rustfs_obs::get_global_guard; +/// +/// async fn trace_operation() -> Result<(), Box> { +/// let guard = get_global_guard()?; +/// let _lock = guard.lock().unwrap(); +/// // Perform traced operation +/// Ok(()) +/// } +/// ``` +pub fn get_global_guard() -> Result>, GuardError> { + GLOBAL_GUARD.get().cloned().ok_or(GuardError::NotInitialized) +} + +/// Try to get the global guard for OpenTelemetry +/// +/// # Returns +/// * `Some(Arc>)` if guard exists +/// * `None` if guard not initialized +pub fn try_get_global_guard() -> Option>> { + GLOBAL_GUARD.get().cloned() +} + +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + async fn test_get_uninitialized_guard() { + let result = get_global_guard(); + assert!(matches!(result, Err(GuardError::NotInitialized))); + } +} diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index 576edf93e..07c5749e2 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -29,12 +29,12 @@ /// ``` mod config; mod entry; +mod global; mod logger; mod sink; mod telemetry; mod utils; mod worker; - pub use config::load_config; pub use config::{AppConfig, OtelConfig}; pub use entry::args::Args; @@ -42,15 +42,15 @@ pub use entry::audit::{ApiDetails, AuditLogEntry}; pub use entry::base::BaseLogEntry; pub use entry::unified::{ConsoleLogEntry, ServerLogEntry, UnifiedLogEntry}; pub use entry::{LogKind, LogRecord, ObjectVersion, SerializableLevel}; -pub use logger::start_logger; -pub use logger::{ - ensure_logger_initialized, get_global_logger, init_global_logger, locked_logger, log_debug, log_error, log_info, log_trace, - log_warn, log_with_context, -}; +pub use global::{get_global_guard, set_global_guard, try_get_global_guard, GuardError}; +pub use logger::{ensure_logger_initialized, log_debug, log_error, log_info, log_trace, log_warn, log_with_context}; +pub use logger::{get_global_logger, init_global_logger, locked_logger, start_logger}; +pub use logger::{log_init_state, InitLogStatus}; pub use logger::{LogError, Logger}; pub use sink::Sink; use std::sync::Arc; -pub use telemetry::{get_global_registry, init_telemetry, metrics_handler}; +pub use telemetry::init_telemetry; +pub use telemetry::{get_global_registry, metrics}; use tokio::sync::Mutex; pub use utils::{get_local_ip, get_local_ip_with_default}; pub use worker::start_worker; diff --git a/packages/obs/src/logger.rs b/packages/obs/src/logger.rs index 22914a666..77862a942 100644 --- a/packages/obs/src/logger.rs +++ b/packages/obs/src/logger.rs @@ -1,5 +1,7 @@ -use crate::{AppConfig, AuditLogEntry, BaseLogEntry, ConsoleLogEntry, ServerLogEntry, Sink, UnifiedLogEntry}; +use crate::global::{ENVIRONMENT, SERVICE_NAME, SERVICE_VERSION}; +use crate::{AppConfig, AuditLogEntry, BaseLogEntry, ConsoleLogEntry, OtelConfig, ServerLogEntry, Sink, UnifiedLogEntry}; use std::sync::Arc; +use std::time::SystemTime; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::sync::{Mutex, OnceCell}; use tracing_core::Level; @@ -471,3 +473,62 @@ pub async fn log_with_context( .write_with_context(message, source, level, request_id, user_id, fields) .await } + +/// Log initialization status +#[derive(Debug)] +pub struct InitLogStatus { + pub timestamp: SystemTime, + pub service_name: String, + pub version: String, + pub environment: String, +} + +impl Default for InitLogStatus { + fn default() -> Self { + Self { + timestamp: SystemTime::now(), + service_name: String::from(SERVICE_NAME), + version: SERVICE_VERSION.to_string(), + environment: ENVIRONMENT.to_string(), + } + } +} + +impl InitLogStatus { + pub fn new_config(config: &OtelConfig) -> Self { + let config = config.clone(); + let environment = config.environment.unwrap_or(ENVIRONMENT.to_string()); + let version = config.service_version.unwrap_or(SERVICE_VERSION.to_string()); + Self { + timestamp: SystemTime::now(), + service_name: String::from(SERVICE_NAME), + version, + environment, + } + } + + pub async fn init_start_log(config: &OtelConfig) -> Result<(), LogError> { + let status = Self::new_config(config); + log_init_state(Some(status)).await + } +} + +/// Log initialization details during system startup +pub async fn log_init_state(status: Option) -> Result<(), LogError> { + let status = status.unwrap_or_default(); + + let base_entry = BaseLogEntry::new() + .timestamp(chrono::DateTime::from(status.timestamp)) + .message(Some(format!( + "Service initialization started - {} v{} in {}", + status.service_name, status.version, status.environment + ))) + .request_id(Some("system_init".to_string())); + + let server_entry = ServerLogEntry::new(Level::INFO, "system_initialization".to_string()) + .with_base(base_entry) + .user_id(Some("system".to_string())); + + get_global_logger().lock().await.log_server_entry(server_entry).await?; + Ok(()) +} diff --git a/packages/obs/src/telemetry.rs b/packages/obs/src/telemetry.rs index 8df3bbf98..f73d9b879 100644 --- a/packages/obs/src/telemetry.rs +++ b/packages/obs/src/telemetry.rs @@ -1,3 +1,4 @@ +use crate::global::{ENVIRONMENT, LOGGER_LEVEL, METER_INTERVAL, SAMPLE_RATIO, SERVICE_NAME, SERVICE_VERSION, USE_STDOUT}; use crate::{get_local_ip_with_default, OtelConfig}; use opentelemetry::trace::TracerProvider; use opentelemetry::{global, KeyValue}; @@ -10,7 +11,7 @@ use opentelemetry_sdk::{ Resource, }; use opentelemetry_semantic_conventions::{ - attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_NAME, SERVICE_VERSION}, + attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_VERSION as OTEL_SERVICE_VERSION}, SCHEMA_URL, }; use prometheus::{Encoder, Registry, TextEncoder}; @@ -45,6 +46,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte /// // When it's dropped, all telemetry components are properly shut down /// drop(otel_guard); /// ``` +#[derive(Debug)] pub struct OtelGuard { tracer_provider: SdkTracerProvider, meter_provider: SdkMeterProvider, @@ -94,14 +96,14 @@ pub fn get_global_registry() -> Arc> { /// /// # Example /// ``` -/// use rustfs_obs::metrics_handler; +/// use rustfs_obs::metrics; /// /// async fn main() { -/// let metrics = metrics_handler().await; +/// let metrics = metrics().await; /// println!("{}", metrics); /// } /// ``` -pub async fn metrics_handler() -> String { +pub async fn metrics() -> String { let encoder = TextEncoder::new(); // Get a reference to the registry for reading metrics let registry = get_global_registry().lock().await.to_owned(); @@ -118,13 +120,13 @@ pub async fn metrics_handler() -> String { /// create OpenTelemetry Resource fn resource(config: &OtelConfig) -> Resource { + let config = config.clone(); Resource::builder() - .with_service_name(config.service_name.clone()) + .with_service_name(config.service_name.unwrap_or(SERVICE_NAME.to_string())) .with_schema_url( [ - KeyValue::new(SERVICE_NAME, config.service_name.clone()), - KeyValue::new(SERVICE_VERSION, config.service_version.clone()), - KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, config.deployment_environment.clone()), + KeyValue::new(OTEL_SERVICE_VERSION, config.service_version.unwrap_or(SERVICE_VERSION.to_string())), + KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, config.environment.unwrap_or(ENVIRONMENT.to_string())), KeyValue::new(NETWORK_LOCAL_ADDRESS, get_local_ip_with_default()), ], SCHEMA_URL, @@ -132,16 +134,19 @@ fn resource(config: &OtelConfig) -> Resource { .build() } +/// Creates a periodic reader for stdout metrics +fn create_periodic_reader(interval: u64) -> PeriodicReader { + PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()) + .with_interval(std::time::Duration::from_secs(interval)) + .build() +} + /// Initialize Meter Provider fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { let mut builder = MeterProviderBuilder::default().with_resource(resource(config)); // If endpoint is empty, use stdout output if config.endpoint.is_empty() { - builder = builder.with_reader( - PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()) - .with_interval(std::time::Duration::from_secs(config.meter_interval)) - .build(), - ); + builder = builder.with_reader(create_periodic_reader(config.meter_interval.unwrap_or(METER_INTERVAL))); } else { // If endpoint is not empty, use otlp output let exporter = opentelemetry_otlp::MetricExporter::builder() @@ -152,16 +157,12 @@ fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { .unwrap(); builder = builder.with_reader( PeriodicReader::builder(exporter) - .with_interval(std::time::Duration::from_secs(config.meter_interval)) + .with_interval(std::time::Duration::from_secs(config.meter_interval.unwrap_or(METER_INTERVAL))) .build(), ); // If use_stdout is true, output to stdout at the same time - if config.use_stdout { - builder = builder.with_reader( - PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()) - .with_interval(std::time::Duration::from_secs(config.meter_interval)) - .build(), - ); + if config.use_stdout.unwrap_or(USE_STDOUT) { + builder = builder.with_reader(create_periodic_reader(config.meter_interval.unwrap_or(METER_INTERVAL))); } } let registry = Registry::new(); @@ -177,8 +178,9 @@ fn init_meter_provider(config: &OtelConfig) -> SdkMeterProvider { /// Initialize Tracer Provider fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { - let sampler = if config.sample_ratio > 0.0 && config.sample_ratio < 1.0 { - Sampler::TraceIdRatioBased(config.sample_ratio) + let sample_ratio = config.sample_ratio.unwrap_or(SAMPLE_RATIO); + let sampler = if sample_ratio > 0.0 && sample_ratio < 1.0 { + Sampler::TraceIdRatioBased(sample_ratio) } else { Sampler::AlwaysOn }; @@ -197,7 +199,7 @@ fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { .with_endpoint(&config.endpoint) .build() .unwrap(); - if config.use_stdout { + if config.use_stdout.unwrap_or(USE_STDOUT) { builder .with_batch_exporter(exporter) .with_batch_exporter(opentelemetry_stdout::SpanExporter::default()) @@ -215,7 +217,6 @@ fn init_tracer_provider(config: &OtelConfig) -> SdkTracerProvider { pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { let tracer_provider = init_tracer_provider(config); let meter_provider = init_meter_provider(config); - let tracer = tracer_provider.tracer(config.service_name.clone()); // Initialize logger provider based on configuration let logger_provider = { @@ -235,30 +236,41 @@ pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { builder = builder.with_batch_exporter(exporter); // Add stdout exporter if requested - if config.use_stdout { + if config.use_stdout.unwrap_or(USE_STDOUT) { builder = builder.with_batch_exporter(opentelemetry_stdout::LogExporter::default()); } } builder.build() }; - + let config = config.clone(); + let logger_level = config.logger_level.unwrap_or(LOGGER_LEVEL.to_string()); + let logger_level = logger_level.as_str(); // Setup OpenTelemetryTracingBridge layer let otel_layer = { // Filter to prevent infinite telemetry loops // This blocks events from OpenTelemetry and its dependent libraries (tonic, reqwest, etc.) // from being sent back to OpenTelemetry itself - let filter_otel = EnvFilter::new("info") - .add_directive("hyper=off".parse().unwrap()) - .add_directive("opentelemetry=off".parse().unwrap()) - .add_directive("tonic=off".parse().unwrap()) - .add_directive("h2=off".parse().unwrap()) - .add_directive("reqwest=off".parse().unwrap()); + let filter_otel = match logger_level { + "trace" | "debug" => { + info!("OpenTelemetry tracing initialized with level: {}", logger_level); + EnvFilter::new(logger_level) + } + _ => { + let mut filter = EnvFilter::new(logger_level); + for directive in ["hyper", "opentelemetry", "tonic", "h2", "reqwest"] { + filter = filter.add_directive(format!("{}=off", directive).parse().unwrap()); + } + filter + } + }; layer::OpenTelemetryTracingBridge::new(&logger_provider).with_filter(filter_otel) }; + + let tracer = tracer_provider.tracer(config.service_name.unwrap_or(SERVICE_NAME.to_string())); let registry = tracing_subscriber::registry() - .with(tracing_subscriber::filter::LevelFilter::INFO) + .with(switch_level(logger_level)) .with(OpenTelemetryLayer::new(tracer)) .with(MetricsLayer::new(meter_provider.clone())) .with(otel_layer); @@ -271,14 +283,13 @@ pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { .with_line_number(true); // Creating a formatting layer with explicit type to avoid type mismatches - let fmt_layer = if config.endpoint.is_empty() { - // Local debug mode: add filter to show OpenTelemetry debug logs - let filter_fmt = EnvFilter::new("info").add_directive("opentelemetry=debug".parse().unwrap()); - fmt_layer.with_filter(filter_fmt) - } else { - // Production mode: use default filter settings - fmt_layer.with_filter(EnvFilter::new("info").add_directive("opentelemetry=off".parse().unwrap())) - }; + let fmt_layer = fmt_layer.with_filter( + EnvFilter::new(logger_level).add_directive( + format!("opentelemetry={}", if config.endpoint.is_empty() { logger_level } else { "off" }) + .parse() + .unwrap(), + ), + ); registry.with(ErrorLayer::default()).with(fmt_layer).init(); if !config.endpoint.is_empty() { @@ -291,3 +302,15 @@ pub fn init_telemetry(config: &OtelConfig) -> OtelGuard { logger_provider, } } + +/// Switch log level +fn switch_level(logger_level: &str) -> tracing_subscriber::filter::LevelFilter { + match logger_level { + "error" => tracing_subscriber::filter::LevelFilter::ERROR, + "warn" => tracing_subscriber::filter::LevelFilter::WARN, + "info" => tracing_subscriber::filter::LevelFilter::INFO, + "debug" => tracing_subscriber::filter::LevelFilter::DEBUG, + "trace" => tracing_subscriber::filter::LevelFilter::TRACE, + _ => tracing_subscriber::filter::LevelFilter::OFF, + } +} diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 08e10859b..6c838ac54 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -34,35 +34,18 @@ use hyper_util::{ service::TowerToHyperService, }; use iam::init_iam_sys; -use once_cell::sync::OnceCell; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; -use rustfs_obs::{init_obs, load_config, BaseLogEntry, ServerLogEntry}; +use rustfs_obs::{init_obs, load_config, set_global_guard, InitLogStatus}; use s3s::{host::MultiDomain, service::S3ServiceBuilder}; use service::hybrid; -use std::time::SystemTime; use std::{io::IsTerminal, net::SocketAddr}; use tokio::net::TcpListener; use tonic::{metadata::MetadataValue, Request, Status}; use tower_http::cors::CorsLayer; use tracing::{debug, error, info, info_span, warn}; -use tracing_core::Level; use tracing_error::ErrorLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -/// Guard that holds a reference to the global observability subsystem. -/// This struct ensures that tracing/logging components remain alive for the -/// application's lifetime. The underlying guard is dropped when this struct -/// is dropped, which may flush logs or perform cleanup operations. -#[allow(dead_code)] -struct TracingGuard(Box); - -impl Drop for TracingGuard { - fn drop(&mut self) { - debug!("Dropping global tracing guard, flushing logs"); - } -} -static GLOBAL_GUARD: OnceCell = OnceCell::new(); - #[allow(dead_code)] fn setup_tracing() { use tracing_subscriber::EnvFilter; @@ -107,34 +90,23 @@ fn print_server_info() { async fn main() -> Result<()> { // Parse the obtained parameters let opt = config::Opt::parse(); - // println!("config: {:?}", &opt); // Load the configuration file let config = load_config(Some(opt.clone().obs_config)); // Initialize Observability - let (logger, guard) = init_obs(config).await; + let (_logger, guard) = init_obs(config.clone()).await; - // Pack and store the guard - GLOBAL_GUARD.set(TracingGuard(Box::new(guard))).unwrap_or_else(|_| { - error!("Unable to set global tracing guard"); - }); + // Store in global storage + set_global_guard(guard)?; - // Initialize the logger - let start_time = SystemTime::now(); - let base_entry = BaseLogEntry::new() - .timestamp(chrono::DateTime::from(start_time)) - .message(Some("main init obs start".to_string())) - .request_id(Some("main".to_string())); - let server_entry = ServerLogEntry::new(Level::INFO, "main_server_entry".to_string()) - .with_base(base_entry) - .user_id(Some("user_id".to_string())); - let _r = logger.lock().await.log_server_entry(server_entry).await; + // Log initialization status + InitLogStatus::init_start_log(&config.observability).await?; // Run parameters run(opt).await } -// + // #[tokio::main] async fn run(opt: config::Opt) -> Result<()> { let span = info_span!("trace-main-run"); @@ -142,7 +114,7 @@ async fn run(opt: config::Opt) -> Result<()> { debug!("opt: {:?}", &opt); - let mut server_addr = net::check_local_server_addr(opt.address.as_str()).unwrap(); + let mut server_addr = net::check_local_server_addr(opt.address.as_str())?; if server_addr.port() == 0 { server_addr.set_port(get_available_port()); @@ -155,8 +127,7 @@ async fn run(opt: config::Opt) -> Result<()> { debug!("server_address {}", &server_address); //设置 AK 和 SK - - iam::init_global_action_cred(Some(opt.access_key.clone()), Some(opt.secret_key.clone())).unwrap(); + iam::init_global_action_cred(Some(opt.access_key.clone()), Some(opt.secret_key.clone()))?; set_global_rustfs_port(server_port); //监听地址,端口从参数中获取 From d12817a772da1701756fef4a87119a9d60dc06d3 Mon Sep 17 00:00:00 2001 From: houseme Date: Thu, 27 Mar 2025 22:21:10 +0800 Subject: [PATCH 21/27] upgrade docker images --- .docker/observability/README.md | 10 +++++----- .docker/observability/docker-compose.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.docker/observability/README.md b/.docker/observability/README.md index f0bd03fcb..a84f7592d 100644 --- a/.docker/observability/README.md +++ b/.docker/observability/README.md @@ -2,11 +2,11 @@ This directory contains the observability stack for the application. The stack is composed of the following components: -- Prometheus -- Grafana -- Loki -- Jaeger -- Otel Collector +- Prometheus v3.2.1 +- Grafana 11.6.0 +- Loki 3.4.2 +- Jaeger 2.4.0 +- Otel Collector 0.120.0 #0.121.0 remove loki ## Prometheus diff --git a/.docker/observability/docker-compose.yml b/.docker/observability/docker-compose.yml index d6bd19323..f6714bb26 100644 --- a/.docker/observability/docker-compose.yml +++ b/.docker/observability/docker-compose.yml @@ -16,7 +16,7 @@ services: networks: - otel-network jaeger: - image: jaegertracing/jaeger:2.3.0 + image: jaegertracing/jaeger:2.4.0 environment: - TZ=Asia/Shanghai ports: @@ -26,7 +26,7 @@ services: networks: - otel-network prometheus: - image: prom/prometheus:v3.2.0 + image: prom/prometheus:v3.2.1 environment: - TZ=Asia/Shanghai volumes: @@ -42,12 +42,12 @@ services: volumes: - ./loki-config.yaml:/etc/loki/local-config.yaml ports: - - "3100:3100" - command: -config.file=/etc/loki/local-config.yaml + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml networks: - otel-network grafana: - image: grafana/grafana:11.5.2 + image: grafana/grafana:11.6.0 ports: - "3000:3000" # Web UI environment: From 2c8b9a83231947df829c89b6fdfafdfd9e37f9fc Mon Sep 17 00:00:00 2001 From: houseme Date: Thu, 27 Mar 2025 23:18:09 +0800 Subject: [PATCH 22/27] webhook add auth_token --- config/obs.example.toml | 3 ++- packages/obs/src/config.rs | 3 ++- packages/obs/src/sink.rs | 26 ++++++++++++++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/config/obs.example.toml b/config/obs.example.toml index 57f454441..4b107e15e 100644 --- a/config/obs.example.toml +++ b/config/obs.example.toml @@ -18,7 +18,8 @@ batch_timeout_ms = 1000 # Default is 1000ms if not specified [sinks.webhook] enabled = false -url = "http://localhost:8080/webhook" +endpoint = "http://localhost:8080/webhook" +auth_token = "" batch_size = 100 # Default is 3 if not specified batch_timeout_ms = 1000 # Default is 100ms if not specified diff --git a/packages/obs/src/config.rs b/packages/obs/src/config.rs index 3acc2efdb..88078fe4c 100644 --- a/packages/obs/src/config.rs +++ b/packages/obs/src/config.rs @@ -51,7 +51,8 @@ pub struct KafkaSinkConfig { #[derive(Debug, Deserialize, Clone, Default)] pub struct WebhookSinkConfig { pub enabled: bool, - pub url: String, + pub endpoint: String, + pub auth_token: String, pub max_retries: Option, // Maximum number of retry times, default 3 pub retry_delay_ms: Option, // Retry the delay cardinality, default 100ms } diff --git a/packages/obs/src/sink.rs b/packages/obs/src/sink.rs index 8eb1830d7..f362d815f 100644 --- a/packages/obs/src/sink.rs +++ b/packages/obs/src/sink.rs @@ -179,7 +179,8 @@ impl Drop for KafkaSink { #[cfg(feature = "webhook")] /// Webhook Sink Implementation pub struct WebhookSink { - url: String, + endpoint: String, + auth_token: String, client: reqwest::Client, max_retries: usize, retry_delay_ms: u64, @@ -187,9 +188,10 @@ pub struct WebhookSink { #[cfg(feature = "webhook")] impl WebhookSink { - pub fn new(url: String, max_retries: usize, retry_delay_ms: u64) -> Self { + pub fn new(endpoint: String, auth_token: String, max_retries: usize, retry_delay_ms: u64) -> Self { WebhookSink { - url, + endpoint, + auth_token, client: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() @@ -205,11 +207,18 @@ impl WebhookSink { impl Sink for WebhookSink { async fn write(&self, entry: &UnifiedLogEntry) { let mut retries = 0; - let url = self.url.clone(); + let url = self.endpoint.clone(); let entry_clone = entry.clone(); - + let auth_value = reqwest::header::HeaderValue::from_str(format!("Bearer {}", self.auth_token.clone()).as_str()).unwrap(); while retries < self.max_retries { - match self.client.post(&url).json(&entry_clone).send().await { + match self + .client + .post(&url) + .header(reqwest::header::AUTHORIZATION, auth_value.clone()) + .json(&entry_clone) + .send() + .await + { Ok(response) if response.status().is_success() => { return; } @@ -234,7 +243,7 @@ impl Drop for WebhookSink { fn drop(&mut self) { // Perform any necessary cleanup here // For example, you might want to log that the sink is being dropped - eprintln!("Dropping WebhookSink with URL: {}", self.url); + eprintln!("Dropping WebhookSink with URL: {}", self.endpoint); } } @@ -410,7 +419,8 @@ pub fn create_sinks(config: &AppConfig) -> Vec> { #[cfg(feature = "webhook")] if config.sinks.webhook.enabled { sinks.push(Arc::new(WebhookSink::new( - config.sinks.webhook.url.clone(), + config.sinks.webhook.endpoint.clone(), + config.sinks.webhook.auth_token.clone(), config.sinks.webhook.max_retries.unwrap_or(3), config.sinks.webhook.retry_delay_ms.unwrap_or(100), ))); From d516eec200a3a4ee0e96e5df0e5458d3d287a5a8 Mon Sep 17 00:00:00 2001 From: houseme Date: Fri, 28 Mar 2025 19:03:09 +0800 Subject: [PATCH 23/27] improve code for config and FileSink --- packages/obs/src/config.rs | 26 ++++++++++++++++---------- packages/obs/src/sink.rs | 16 ++++++++++++++-- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/obs/src/config.rs b/packages/obs/src/config.rs index 88078fe4c..be20c5b0b 100644 --- a/packages/obs/src/config.rs +++ b/packages/obs/src/config.rs @@ -142,14 +142,20 @@ const DEFAULT_CONFIG_FILE: &str = "obs"; /// ``` pub fn load_config(config_dir: Option) -> AppConfig { let config_dir = if let Some(path) = config_dir { - // Use the provided path - let path = std::path::Path::new(&path); - if path.extension().is_some() { - // If path has extension, use it as is (extension will be added by Config::builder) - path.with_extension("").to_string_lossy().into_owned() + // If a path is provided, check if it's empty + if path.is_empty() { + // If empty, use the default config file name + DEFAULT_CONFIG_FILE.to_string() } else { - // If path is a directory, append the default config file name - path.to_string_lossy().into_owned() + // Use the provided path + let path = std::path::Path::new(&path); + if path.extension().is_some() { + // If path has extension, use it as is (extension will be added by Config::builder) + path.with_extension("").to_string_lossy().into_owned() + } else { + // If path is a directory, append the default config file name + path.to_string_lossy().into_owned() + } } } else { // If no path provided, use current directory + default config file @@ -166,11 +172,11 @@ pub fn load_config(config_dir: Option) -> AppConfig { println!("Using config file base: {}", config_dir); let config = Config::builder() - .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml)) + .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml).required(false)) .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Yaml).required(false)) .add_source(config::Environment::with_prefix("")) .build() - .unwrap(); + .unwrap_or_default(); - config.try_deserialize().unwrap() + config.try_deserialize().unwrap_or_default() } diff --git a/packages/obs/src/sink.rs b/packages/obs/src/sink.rs index f362d815f..cc3aed3e0 100644 --- a/packages/obs/src/sink.rs +++ b/packages/obs/src/sink.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use tokio::fs::OpenOptions; use tokio::io; use tokio::io::AsyncWriteExt; +use tracing::debug; /// Sink Trait definition, asynchronously write logs #[async_trait] @@ -268,8 +269,19 @@ impl FileSink { flush_interval_ms: u64, flush_threshold: usize, ) -> Result { - let file = OpenOptions::new().append(true).create(true).open(&path).await?; - + // check if the file exists + let file_exists = tokio::fs::metadata(&path).await.is_ok(); + let file = if file_exists { + // If the file exists, open it in append mode + debug!("FileSink: File exists, opening in append mode."); + OpenOptions::new().append(true).create(true).open(&path).await? + } else { + // If the file does not exist, create it + debug!("FileSink: File does not exist, creating a new file."); + // Create the file and write a header or initial content if needed + OpenOptions::new().create(true).write(true).open(&path).await? + }; + // let file = OpenOptions::new().append(true).create(true).open(&path).await?; let writer = io::BufWriter::with_capacity(buffer_size, file); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) From c87e50b002ae89f9c87a23238ba939e086cc6829 Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 30 Mar 2025 21:28:29 +0800 Subject: [PATCH 24/27] improve code for FileSink --- .gitignore | 3 ++- packages/obs/src/global.rs | 2 +- packages/obs/src/lib.rs | 2 +- packages/obs/src/sink.rs | 46 +++++++++++++++----------------------- 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 7017949a1..53e10cf27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ rustfs/static/* vendor cli/rustfs-gui/embedded-rustfs/rustfs -config/obs.toml \ No newline at end of file +config/obs.toml +*.log \ No newline at end of file diff --git a/packages/obs/src/global.rs b/packages/obs/src/global.rs index 86297b8b4..41c5e555e 100644 --- a/packages/obs/src/global.rs +++ b/packages/obs/src/global.rs @@ -8,7 +8,7 @@ pub(crate) const SERVICE_NAME: &str = "RustFS"; pub(crate) const SAMPLE_RATIO: f64 = 1.0; pub(crate) const METER_INTERVAL: u64 = 60; pub(crate) const SERVICE_VERSION: &str = "0.1.0"; -pub(crate) const ENVIRONMENT: &str = "development"; +pub(crate) const ENVIRONMENT: &str = "production"; pub(crate) const LOGGER_LEVEL: &str = "info"; /// Global guard for OpenTelemetry tracing diff --git a/packages/obs/src/lib.rs b/packages/obs/src/lib.rs index 07c5749e2..843c52367 100644 --- a/packages/obs/src/lib.rs +++ b/packages/obs/src/lib.rs @@ -72,7 +72,7 @@ pub use worker::start_worker; /// ``` pub async fn init_obs(config: AppConfig) -> (Arc>, telemetry::OtelGuard) { let guard = init_telemetry(&config.observability); - let sinks = sink::create_sinks(&config); + let sinks = sink::create_sinks(&config).await; let logger = init_global_logger(&config, sinks).await; (logger, guard) } diff --git a/packages/obs/src/sink.rs b/packages/obs/src/sink.rs index cc3aed3e0..4a44f0ea4 100644 --- a/packages/obs/src/sink.rs +++ b/packages/obs/src/sink.rs @@ -262,7 +262,7 @@ pub struct FileSink { #[cfg(feature = "file")] impl FileSink { - #[allow(dead_code)] + /// Create a new FileSink instance pub async fn new( path: String, buffer_size: usize, @@ -271,6 +271,11 @@ impl FileSink { ) -> Result { // check if the file exists let file_exists = tokio::fs::metadata(&path).await.is_ok(); + // if the file not exists, create it + if !file_exists { + tokio::fs::create_dir_all(std::path::Path::new(&path).parent().unwrap()).await?; + debug!("the file not exists,create if. path: {:?}", path) + } let file = if file_exists { // If the file exists, open it in append mode debug!("FileSink: File exists, opening in append mode."); @@ -281,7 +286,6 @@ impl FileSink { // Create the file and write a header or initial content if needed OpenOptions::new().create(true).write(true).open(&path).await? }; - // let file = OpenOptions::new().append(true).create(true).open(&path).await?; let writer = io::BufWriter::with_capacity(buffer_size, file); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -406,7 +410,7 @@ impl Drop for FileSink { } /// Create a list of Sink instances -pub fn create_sinks(config: &AppConfig) -> Vec> { +pub async fn create_sinks(config: &AppConfig) -> Vec> { let mut sinks: Vec> = Vec::new(); #[cfg(feature = "kafka")] @@ -445,31 +449,17 @@ pub fn create_sinks(config: &AppConfig) -> Vec> { } else { "default.log".to_string() }; - - // Use synchronous file operations - let file_result = std::fs::OpenOptions::new().append(true).create(true).open(&path); - match file_result { - Ok(file) => { - let buffer_size = config.sinks.file.buffer_size.unwrap_or(8192); - let writer = io::BufWriter::with_capacity(buffer_size, tokio::fs::File::from_std(file)); - - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - - sinks.push(Arc::new(FileSink { - path: path.clone(), - buffer_size, - writer: Arc::new(tokio::sync::Mutex::new(writer)), - entry_count: std::sync::atomic::AtomicUsize::new(0), - last_flush: std::sync::atomic::AtomicU64::new(now), - flush_interval_ms: config.sinks.file.flush_interval_ms.unwrap_or(1000), - flush_threshold: config.sinks.file.flush_threshold.unwrap_or(100), - })); - } - Err(e) => eprintln!("Failed to create file sink: {}", e), - } + debug!("FileSink: Using path: {}", path); + sinks.push(Arc::new( + FileSink::new( + path.clone(), + config.sinks.file.buffer_size.unwrap_or(8192), + config.sinks.file.flush_interval_ms.unwrap_or(1000), + config.sinks.file.flush_threshold.unwrap_or(100), + ) + .await + .unwrap(), + )); } sinks From 96cc708b5b2c0af7bfabfc5b413c9863704631ed Mon Sep 17 00:00:00 2001 From: houseme Date: Mon, 31 Mar 2025 16:32:22 +0800 Subject: [PATCH 25/27] upgrade reqwest version from 0.12.12 to 0.12.15 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 665a4d7ce..29f07cb48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ protobuf = "3.7" protos = { path = "./common/protos" } rand = "0.8.5" rdkafka = { version = "0.37", features = ["tokio"] } -reqwest = { version = "0.12.12", default-features = false, features = ["rustls-tls", "charset", "http2", "macos-system-configuration", "stream", "json", "blocking"] } +reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "charset", "http2", "macos-system-configuration", "stream", "json", "blocking"] } rfd = { version = "0.15.2", default-features = false, features = ["xdg-portal", "tokio"] } rmp = "0.8.14" rmp-serde = "1.3.0" From f2692b78dd36a2803ead3e713f3f6429ed96c97d Mon Sep 17 00:00:00 2001 From: houseme Date: Mon, 31 Mar 2025 18:32:22 +0800 Subject: [PATCH 26/27] TryInto cover --- Cargo.toml | 2 +- ecstore/src/heal/data_scanner.rs | 24 +++++++++--------------- ecstore/src/heal/data_usage_cache.rs | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 29f07cb48..342c1ea5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ serde_json = "1.0.138" sha2 = "0.10.8" tempfile = "3.16.0" thiserror = "2.0.12" -time = { version = "0.3.37", features = [ +time = { version = "0.3.41", features = [ "std", "parsing", "formatting", diff --git a/ecstore/src/heal/data_scanner.rs b/ecstore/src/heal/data_scanner.rs index 52ded12a2..6701f7ba8 100644 --- a/ecstore/src/heal/data_scanner.rs +++ b/ecstore/src/heal/data_scanner.rs @@ -352,7 +352,7 @@ impl CurrentScannerCycle { let str_len = rmp::decode::read_str_len(&mut cur)?; - // !!! Vec::with_capacity(str_len) 失败,vec!正常 + // !!!Vec::with_capacity(str_len) 失败,vec! 正常 let mut field_buff = vec![0u8; str_len as usize]; cur.read_exact(&mut field_buff)?; @@ -435,7 +435,7 @@ impl ScannerItem { pub async fn apply_versions_actions(&self, fivs: &[FileInfo]) -> Result> { let obj_infos = self.apply_newer_noncurrent_version_limit(fivs).await?; - if obj_infos.len() >= SCANNER_EXCESS_OBJECT_VERSIONS.load(Ordering::SeqCst).try_into().unwrap() { + if obj_infos.len() >= >::try_into(SCANNER_EXCESS_OBJECT_VERSIONS.load(Ordering::SeqCst)).unwrap() { // todo } @@ -445,10 +445,7 @@ impl ScannerItem { } if cumulative_size - >= SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL_SIZE - .load(Ordering::SeqCst) - .try_into() - .unwrap() + >= >::try_into(SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL_SIZE.load(Ordering::SeqCst)).unwrap() { //todo } @@ -684,16 +681,13 @@ impl FolderScanner { } let should_compact = self.new_cache.info.name != folder.name - && existing_folders.len() + new_folders.len() >= DATA_SCANNER_COMPACT_AT_FOLDERS.try_into().unwrap() - || existing_folders.len() + new_folders.len() >= DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS.try_into().unwrap(); + && existing_folders.len() + new_folders.len() + >= >::try_into(DATA_SCANNER_COMPACT_AT_FOLDERS).unwrap() + || existing_folders.len() + new_folders.len() + >= >::try_into(DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS).unwrap(); let total_folders = existing_folders.len() + new_folders.len(); - if total_folders - > SCANNER_EXCESS_FOLDERS - .load(std::sync::atomic::Ordering::SeqCst) - .try_into() - .unwrap() - { + if total_folders > >::try_into(SCANNER_EXCESS_FOLDERS.load(Ordering::SeqCst)).unwrap() { let _prefix_name = format!("{}/", folder.name.trim_end_matches('/')); // todo: notification } @@ -957,7 +951,7 @@ impl FolderScanner { if !into.compacted && self.new_cache.info.name != folder.name { let mut flat = self.new_cache.size_recursive(&this_hash.key()).unwrap_or_default(); flat.compacted = true; - let compact = if flat.objects < DATA_SCANNER_COMPACT_LEAST_OBJECT.try_into().unwrap() { + let compact = if flat.objects < >::try_into(DATA_SCANNER_COMPACT_LEAST_OBJECT).unwrap() { true } else { // Compact if we only have objects as children... diff --git a/ecstore/src/heal/data_usage_cache.rs b/ecstore/src/heal/data_usage_cache.rs index 7bc33df81..9111d2a7f 100644 --- a/ecstore/src/heal/data_usage_cache.rs +++ b/ecstore/src/heal/data_usage_cache.rs @@ -588,7 +588,7 @@ impl DataUsageCache { Some(e) => e, None => return, }; - if top_e.children.len() > DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS.try_into().unwrap() { + if top_e.children.len() > >::try_into(DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS).unwrap() { self.reduce_children_of(&hash_path(&self.info.name), limit, true); } if self.cache.len() <= limit { From c57ee8e4712ecee28595cd93ac2ddb4f2bbf1a1e Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 2 Apr 2025 16:08:51 +0800 Subject: [PATCH 27/27] modify default value --- config/obs.example.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/obs.example.toml b/config/obs.example.toml index 4b107e15e..0ba434ada 100644 --- a/config/obs.example.toml +++ b/config/obs.example.toml @@ -1,7 +1,7 @@ [observability] -endpoint = "" # Default is "http://localhost:4317" if not specified -use_stdout = false -sample_ratio = 0.5 +endpoint = "http://localhost:4317" # Default is "http://localhost:4317" if not specified +use_stdout = false # Output with stdout, true output, false no output +sample_ratio = 2.0 meter_interval = 30 service_name = "rustfs" service_version = "0.1.0"