feat(logging): Emit a setup error when a restricted keys are used for logging default keys (#5185)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Abhishek Kanojia
2024-07-14 19:28:21 +05:30
committed by GitHub
parent 21499947ad
commit ff96a62b95
14 changed files with 151 additions and 176 deletions

7
Cargo.lock generated
View File

@ -2002,6 +2002,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"strum 0.26.2", "strum 0.26.2",
"thiserror",
"utoipa", "utoipa",
] ]
@ -6219,6 +6220,7 @@ name = "router_env"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cargo_metadata 0.18.1", "cargo_metadata 0.18.1",
"common_enums",
"config", "config",
"error-stack", "error-stack",
"gethostname", "gethostname",
@ -7238,6 +7240,7 @@ dependencies = [
"async-trait", "async-trait",
"bb8", "bb8",
"bytes 1.6.0", "bytes 1.6.0",
"common_enums",
"common_utils", "common_utils",
"config", "config",
"crc32fast", "crc32fast",
@ -8439,9 +8442,9 @@ checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "utoipa" name = "utoipa"

View File

@ -13,6 +13,7 @@ openapi = []
payouts = [] payouts = []
[dependencies] [dependencies]
thiserror = "1.0.58"
diesel = { version = "2.1.5", features = ["postgres"] } diesel = { version = "2.1.5", features = ["postgres"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115" serde_json = "1.0.115"

View File

@ -20,6 +20,80 @@ pub mod diesel_exports {
}; };
} }
pub type ApplicationResult<T> = Result<T, ApplicationError>;
#[derive(Debug, thiserror::Error)]
pub enum ApplicationError {
#[error("Application configuration error")]
ConfigurationError,
#[error("Invalid configuration value provided: {0}")]
InvalidConfigurationValueError(String),
#[error("Metrics error")]
MetricsError,
#[error("I/O: {0}")]
IoError(std::io::Error),
#[error("Error while constructing api client: {0}")]
ApiClientError(ApiClientError),
}
#[derive(Debug, thiserror::Error, PartialEq, Clone)]
pub enum ApiClientError {
#[error("Header map construction failed")]
HeaderMapConstructionFailed,
#[error("Invalid proxy configuration")]
InvalidProxyConfiguration,
#[error("Client construction failed")]
ClientConstructionFailed,
#[error("Certificate decode failed")]
CertificateDecodeFailed,
#[error("Request body serialization failed")]
BodySerializationFailed,
#[error("Unexpected state reached/Invariants conflicted")]
UnexpectedState,
#[error("URL encoding of request payload failed")]
UrlEncodingFailed,
#[error("Failed to send request to connector {0}")]
RequestNotSent(String),
#[error("Failed to decode response")]
ResponseDecodingFailed,
#[error("Server responded with Request Timeout")]
RequestTimeoutReceived,
#[error("connection closed before a message could complete")]
ConnectionClosedIncompleteMessage,
#[error("Server responded with Internal Server Error")]
InternalServerErrorReceived,
#[error("Server responded with Bad Gateway")]
BadGatewayReceived,
#[error("Server responded with Service Unavailable")]
ServiceUnavailableReceived,
#[error("Server responded with Gateway Timeout")]
GatewayTimeoutReceived,
#[error("Server responded with unexpected response")]
UnexpectedServerResponse,
}
impl ApiClientError {
pub fn is_upstream_timeout(&self) -> bool {
self == &Self::RequestTimeoutReceived
}
pub fn is_connection_closed_before_message_could_complete(&self) -> bool {
self == &Self::ConnectionClosedIncompleteMessage
}
}
impl From<std::io::Error> for ApplicationError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
/// The status of the attempt /// The status of the attempt
#[derive( #[derive(
Clone, Clone,

View File

@ -1,8 +1,8 @@
//! Configs interface //! Configs interface
use common_enums::ApplicationError;
use masking::Secret; use masking::Secret;
use router_derive; use router_derive;
use serde::Deserialize; use serde::Deserialize;
use storage_impl::errors::ApplicationError;
// struct Connectors // struct Connectors
#[allow(missing_docs, missing_debug_implementations)] #[allow(missing_docs, missing_debug_implementations)]

View File

@ -1,3 +1,4 @@
use error_stack::ResultExt;
use router::{ use router::{
configs::settings::{CmdLineConf, Settings}, configs::settings::{CmdLineConf, Settings},
core::errors::{ApplicationError, ApplicationResult}, core::errors::{ApplicationError, ApplicationResult},
@ -24,7 +25,8 @@ async fn main() -> ApplicationResult<()> {
&conf.log, &conf.log,
router_env::service_name!(), router_env::service_name!(),
[router_env::service_name!(), "actix_server"], [router_env::service_name!(), "actix_server"],
); )
.change_context(ApplicationError::ConfigurationError)?;
logger::info!("Application started [{:?}] [{:?}]", conf.server, conf.log); logger::info!("Application started [{:?}] [{:?}]", conf.server, conf.log);
@ -39,8 +41,7 @@ async fn main() -> ApplicationResult<()> {
.expect("Failed to create the server"); .expect("Failed to create the server");
let _ = server.await; let _ = server.await;
Err(ApplicationError::from(std::io::Error::new( Err(error_stack::Report::from(ApplicationError::from(
std::io::ErrorKind::Other, std::io::Error::new(std::io::ErrorKind::Other, "Server shut down"),
"Server shut down",
))) )))
} }

View File

@ -116,7 +116,8 @@ pub async fn start_web_server(
let web_server = actix_web::HttpServer::new(move || { let web_server = actix_web::HttpServer::new(move || {
actix_web::App::new().service(Health::server(state.clone(), service.clone())) actix_web::App::new().service(Health::server(state.clone(), service.clone()))
}) })
.bind((server.host.as_str(), server.port))? .bind((server.host.as_str(), server.port))
.change_context(ApplicationError::ConfigurationError)?
.workers(server.workers) .workers(server.workers)
.run(); .run();
let _ = web_server.handle(); let _ = web_server.handle();

View File

@ -8,6 +8,7 @@ use analytics::{opensearch::OpenSearchConfig, ReportConfig};
use api_models::{enums, payment_methods::RequiredFieldInfo}; use api_models::{enums, payment_methods::RequiredFieldInfo};
use common_utils::ext_traits::ConfigExt; use common_utils::ext_traits::ConfigExt;
use config::{Environment, File}; use config::{Environment, File};
use error_stack::ResultExt;
#[cfg(feature = "email")] #[cfg(feature = "email")]
use external_services::email::EmailSettings; use external_services::email::EmailSettings;
use external_services::{ use external_services::{
@ -33,7 +34,7 @@ use storage_impl::config::QueueStrategy;
use crate::analytics::AnalyticsConfig; use crate::analytics::AnalyticsConfig;
use crate::{ use crate::{
core::errors::{ApplicationError, ApplicationResult}, core::errors::{ApplicationError, ApplicationResult},
env::{self, logger, Env}, env::{self, Env},
events::EventsConfig, events::EventsConfig,
}; };
@ -722,7 +723,8 @@ impl Settings<SecuredSecret> {
let environment = env::which(); let environment = env::which();
let config_path = router_env::Config::config_path(&environment.to_string(), config_path); let config_path = router_env::Config::config_path(&environment.to_string(), config_path);
let config = router_env::Config::builder(&environment.to_string())? let config = router_env::Config::builder(&environment.to_string())
.change_context(ApplicationError::ConfigurationError)?
.add_source(File::from(config_path).required(false)) .add_source(File::from(config_path).required(false))
.add_source( .add_source(
Environment::with_prefix("ROUTER") Environment::with_prefix("ROUTER")
@ -736,13 +738,12 @@ impl Settings<SecuredSecret> {
.with_list_parse_key("connector_request_reference_id_config.merchant_ids_send_payment_id_as_connector_request_id"), .with_list_parse_key("connector_request_reference_id_config.merchant_ids_send_payment_id_as_connector_request_id"),
) )
.build()?; .build()
.change_context(ApplicationError::ConfigurationError)?;
serde_path_to_error::deserialize(config).map_err(|error| { serde_path_to_error::deserialize(config)
logger::error!(%error, "Unable to deserialize application configuration"); .attach_printable("Unable to deserialize application configuration")
eprintln!("Unable to deserialize application configuration: {error}"); .change_context(ApplicationError::ConfigurationError)
ApplicationError::from(error.into_inner())
})
} }
pub fn validate(&self) -> ApplicationResult<()> { pub fn validate(&self) -> ApplicationResult<()> {
@ -756,14 +757,18 @@ impl Settings<SecuredSecret> {
})?; })?;
if self.log.file.enabled { if self.log.file.enabled {
if self.log.file.file_name.is_default_or_empty() { if self.log.file.file_name.is_default_or_empty() {
return Err(ApplicationError::InvalidConfigurationValueError( return Err(error_stack::Report::from(
"log file name must not be empty".into(), ApplicationError::InvalidConfigurationValueError(
"log file name must not be empty".into(),
),
)); ));
} }
if self.log.file.path.is_default_or_empty() { if self.log.file.path.is_default_or_empty() {
return Err(ApplicationError::InvalidConfigurationValueError( return Err(error_stack::Report::from(
"log directory path must not be empty".into(), ApplicationError::InvalidConfigurationValueError(
"log directory path must not be empty".into(),
),
)); ));
} }
} }

View File

@ -32,7 +32,7 @@ use crate::services;
pub type RouterResult<T> = CustomResult<T, ApiErrorResponse>; pub type RouterResult<T> = CustomResult<T, ApiErrorResponse>;
pub type RouterResponse<T> = CustomResult<services::ApplicationResponse<T>, ApiErrorResponse>; pub type RouterResponse<T> = CustomResult<services::ApplicationResponse<T>, ApiErrorResponse>;
pub type ApplicationResult<T> = Result<T, ApplicationError>; pub type ApplicationResult<T> = error_stack::Result<T, ApplicationError>;
pub type ApplicationResponse<T> = ApplicationResult<services::ApplicationResponse<T>>; pub type ApplicationResponse<T> = ApplicationResult<services::ApplicationResponse<T>>;
pub type CustomerResponse<T> = pub type CustomerResponse<T> =

View File

@ -14,6 +14,7 @@ error-stack = "0.4.1"
gethostname = "0.4.3" gethostname = "0.4.3"
once_cell = "1.19.0" once_cell = "1.19.0"
opentelemetry = { version = "0.19.0", features = ["rt-tokio-current-thread", "metrics"] } opentelemetry = { version = "0.19.0", features = ["rt-tokio-current-thread", "metrics"] }
common_enums = { path = "../common_enums" }
opentelemetry-otlp = { version = "0.12.0", features = ["metrics"] } opentelemetry-otlp = { version = "0.12.0", features = ["metrics"] }
rustc-hash = "1.1" rustc-hash = "1.1"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }

View File

@ -8,6 +8,7 @@ use std::{
io::Write, io::Write,
}; };
use config::ConfigError;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::ser::{SerializeMap, Serializer}; use serde::ser::{SerializeMap, Serializer};
use serde_json::{ser::Formatter, Value}; use serde_json::{ser::Formatter, Value};
@ -155,7 +156,11 @@ where
/// let formatting_layer = router_env::FormattingLayer::new("my_service", std::io::stdout, serde_json::ser::CompactFormatter); /// let formatting_layer = router_env::FormattingLayer::new("my_service", std::io::stdout, serde_json::ser::CompactFormatter);
/// ``` /// ```
/// ///
pub fn new(service: &str, dst_writer: W, formatter: F) -> Self { pub fn new(
service: &str,
dst_writer: W,
formatter: F,
) -> error_stack::Result<Self, ConfigError> {
Self::new_with_implicit_entries(service, dst_writer, HashMap::new(), formatter) Self::new_with_implicit_entries(service, dst_writer, HashMap::new(), formatter)
} }
@ -163,9 +168,9 @@ where
pub fn new_with_implicit_entries( pub fn new_with_implicit_entries(
service: &str, service: &str,
dst_writer: W, dst_writer: W,
mut default_fields: HashMap<String, Value>, default_fields: HashMap<String, Value>,
formatter: F, formatter: F,
) -> Self { ) -> error_stack::Result<Self, ConfigError> {
let pid = std::process::id(); let pid = std::process::id();
let hostname = gethostname::gethostname().to_string_lossy().into_owned(); let hostname = gethostname::gethostname().to_string_lossy().into_owned();
let service = service.to_string(); let service = service.to_string();
@ -174,20 +179,16 @@ where
#[cfg(feature = "vergen")] #[cfg(feature = "vergen")]
let build = crate::build!().to_string(); let build = crate::build!().to_string();
let env = crate::env::which().to_string(); let env = crate::env::which().to_string();
default_fields.retain(|key, value| { for key in default_fields.keys() {
if !IMPLICIT_KEYS.contains(key.as_str()) { if IMPLICIT_KEYS.contains(key.as_str()) {
true return Err(ConfigError::Message(format!(
} else { "A reserved key `{key}` was included in `default_fields` in the log formatting layer"
tracing::warn!( ))
?key, .into());
?value,
"Attempting to log a reserved entry. It won't be added to the logs"
);
false
} }
}); }
Self { Ok(Self {
dst_writer, dst_writer,
pid, pid,
hostname, hostname,
@ -199,7 +200,7 @@ where
build, build,
default_fields, default_fields,
formatter, formatter,
} })
} }
/// Serialize common for both span and event entries. /// Serialize common for both span and event entries.

View File

@ -2,6 +2,7 @@
use std::time::Duration; use std::time::Duration;
use ::config::ConfigError;
use opentelemetry::{ use opentelemetry::{
global, runtime, global, runtime,
sdk::{ sdk::{
@ -36,7 +37,7 @@ pub fn setup(
config: &config::Log, config: &config::Log,
service_name: &str, service_name: &str,
crates_to_filter: impl AsRef<[&'static str]>, crates_to_filter: impl AsRef<[&'static str]>,
) -> TelemetryGuard { ) -> error_stack::Result<TelemetryGuard, ConfigError> {
let mut guards = Vec::new(); let mut guards = Vec::new();
// Setup OpenTelemetry traces and metrics // Setup OpenTelemetry traces and metrics
@ -69,11 +70,9 @@ pub fn setup(
&crates_to_filter, &crates_to_filter,
); );
println!("Using file logging filter: {file_filter}"); println!("Using file logging filter: {file_filter}");
let layer = FormattingLayer::new(service_name, file_writer, CompactFormatter)?
Some( .with_filter(file_filter);
FormattingLayer::new(service_name, file_writer, CompactFormatter) Some(layer)
.with_filter(file_filter),
)
} else { } else {
None None
}; };
@ -107,17 +106,21 @@ pub fn setup(
} }
config::LogFormat::Json => { config::LogFormat::Json => {
error_stack::Report::set_color_mode(error_stack::fmt::ColorMode::None); error_stack::Report::set_color_mode(error_stack::fmt::ColorMode::None);
let logging_layer = subscriber
FormattingLayer::new(service_name, console_writer, CompactFormatter) .with(
.with_filter(console_filter); FormattingLayer::new(service_name, console_writer, CompactFormatter)?
subscriber.with(logging_layer).init(); .with_filter(console_filter),
)
.init();
} }
config::LogFormat::PrettyJson => { config::LogFormat::PrettyJson => {
error_stack::Report::set_color_mode(error_stack::fmt::ColorMode::None); error_stack::Report::set_color_mode(error_stack::fmt::ColorMode::None);
let logging_layer = subscriber
FormattingLayer::new(service_name, console_writer, PrettyFormatter::new()) .with(
.with_filter(console_filter); FormattingLayer::new(service_name, console_writer, PrettyFormatter::new())?
subscriber.with(logging_layer).init(); .with_filter(console_filter),
)
.init();
} }
} }
} else { } else {
@ -126,10 +129,10 @@ pub fn setup(
// Returning the TelemetryGuard for logs to be printed and metrics to be collected until it is // Returning the TelemetryGuard for logs to be printed and metrics to be collected until it is
// dropped // dropped
TelemetryGuard { Ok(TelemetryGuard {
_log_guards: guards, _log_guards: guards,
_metrics_controller, _metrics_controller,
} })
} }
fn get_opentelemetry_exporter(config: &config::LogTelemetry) -> TonicExporterBuilder { fn get_opentelemetry_exporter(config: &config::LogTelemetry) -> TonicExporterBuilder {

View File

@ -1,25 +1,25 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
mod test_module; mod test_module;
use ::config::ConfigError;
use router_env::TelemetryGuard; use router_env::TelemetryGuard;
use self::test_module::some_module::*; use self::test_module::some_module::*;
fn logger() -> &'static TelemetryGuard { fn logger() -> error_stack::Result<&'static TelemetryGuard, ConfigError> {
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
static INSTANCE: OnceCell<TelemetryGuard> = OnceCell::new(); static INSTANCE: OnceCell<TelemetryGuard> = OnceCell::new();
INSTANCE.get_or_init(|| { Ok(INSTANCE.get_or_init(|| {
let config = router_env::Config::new().unwrap(); let config = router_env::Config::new().unwrap();
router_env::setup(&config.log, "router_env_test", []) router_env::setup(&config.log, "router_env_test", []).unwrap()
}) }))
} }
#[tokio::test] #[tokio::test]
async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
logger(); logger()?;
fn_with_colon(13).await; fn_with_colon(13).await;

View File

@ -18,6 +18,7 @@ payouts = ["hyperswitch_domain_models/payouts"]
# First Party dependencies # First Party dependencies
api_models = { version = "0.1.0", path = "../api_models" } api_models = { version = "0.1.0", path = "../api_models" }
common_utils = { version = "0.1.0", path = "../common_utils" } common_utils = { version = "0.1.0", path = "../common_utils" }
common_enums = { version = "0.1.0", path = "../common_enums" }
hyperswitch_domain_models = { version = "0.1.0", path = "../hyperswitch_domain_models", default-features = false } hyperswitch_domain_models = { version = "0.1.0", path = "../hyperswitch_domain_models", default-features = false }
diesel_models = { version = "0.1.0", path = "../diesel_models" } diesel_models = { version = "0.1.0", path = "../diesel_models" }
masking = { version = "0.1.0", path = "../masking" } masking = { version = "0.1.0", path = "../masking" }

View File

@ -1,17 +1,10 @@
use std::fmt::Display; pub use common_enums::{ApiClientError, ApplicationError, ApplicationResult};
use actix_web::ResponseError;
use common_utils::errors::ErrorSwitch; use common_utils::errors::ErrorSwitch;
use config::ConfigError;
use http::StatusCode;
use hyperswitch_domain_models::errors::StorageError as DataStorageError; use hyperswitch_domain_models::errors::StorageError as DataStorageError;
pub use redis_interface::errors::RedisError; pub use redis_interface::errors::RedisError;
use router_env::opentelemetry::metrics::MetricsError;
use crate::store::errors::DatabaseError; use crate::store::errors::DatabaseError;
pub type ApplicationResult<T> = Result<T, ApplicationError>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum StorageError { pub enum StorageError {
#[error("DatabaseError: {0:?}")] #[error("DatabaseError: {0:?}")]
@ -154,115 +147,6 @@ impl RedisErrorExt for error_stack::Report<RedisError> {
} }
} }
#[derive(Debug, thiserror::Error)]
pub enum ApplicationError {
// Display's impl can be overridden by the attribute error marco.
// Don't use Debug here, Debug gives error stack in response.
#[error("Application configuration error: {0}")]
ConfigurationError(ConfigError),
#[error("Invalid configuration value provided: {0}")]
InvalidConfigurationValueError(String),
#[error("Metrics error: {0}")]
MetricsError(MetricsError),
#[error("I/O: {0}")]
IoError(std::io::Error),
#[error("Error while constructing api client: {0}")]
ApiClientError(ApiClientError),
}
impl From<MetricsError> for ApplicationError {
fn from(err: MetricsError) -> Self {
Self::MetricsError(err)
}
}
impl From<std::io::Error> for ApplicationError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<ConfigError> for ApplicationError {
fn from(err: ConfigError) -> Self {
Self::ConfigurationError(err)
}
}
fn error_response<T: Display>(err: &T) -> actix_web::HttpResponse {
actix_web::HttpResponse::BadRequest()
.content_type(mime::APPLICATION_JSON)
.body(format!(r#"{{ "error": {{ "message": "{err}" }} }}"#))
}
impl ResponseError for ApplicationError {
fn status_code(&self) -> StatusCode {
match self {
Self::MetricsError(_)
| Self::IoError(_)
| Self::ConfigurationError(_)
| Self::InvalidConfigurationValueError(_)
| Self::ApiClientError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> actix_web::HttpResponse {
error_response(self)
}
}
#[derive(Debug, thiserror::Error, PartialEq, Clone)]
pub enum ApiClientError {
#[error("Header map construction failed")]
HeaderMapConstructionFailed,
#[error("Invalid proxy configuration")]
InvalidProxyConfiguration,
#[error("Client construction failed")]
ClientConstructionFailed,
#[error("Certificate decode failed")]
CertificateDecodeFailed,
#[error("Request body serialization failed")]
BodySerializationFailed,
#[error("Unexpected state reached/Invariants conflicted")]
UnexpectedState,
#[error("URL encoding of request payload failed")]
UrlEncodingFailed,
#[error("Failed to send request to connector {0}")]
RequestNotSent(String),
#[error("Failed to decode response")]
ResponseDecodingFailed,
#[error("Server responded with Request Timeout")]
RequestTimeoutReceived,
#[error("connection closed before a message could complete")]
ConnectionClosedIncompleteMessage,
#[error("Server responded with Internal Server Error")]
InternalServerErrorReceived,
#[error("Server responded with Bad Gateway")]
BadGatewayReceived,
#[error("Server responded with Service Unavailable")]
ServiceUnavailableReceived,
#[error("Server responded with Gateway Timeout")]
GatewayTimeoutReceived,
#[error("Server responded with unexpected response")]
UnexpectedServerResponse,
}
impl ApiClientError {
pub fn is_upstream_timeout(&self) -> bool {
self == &Self::RequestTimeoutReceived
}
pub fn is_connection_closed_before_message_could_complete(&self) -> bool {
self == &Self::ConnectionClosedIncompleteMessage
}
}
#[derive(Debug, thiserror::Error, PartialEq)] #[derive(Debug, thiserror::Error, PartialEq)]
pub enum ConnectorError { pub enum ConnectorError {
#[error("Error while obtaining URL for the integration")] #[error("Error while obtaining URL for the integration")]