-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Overview
Similar to #182, the problem of modular logging can be solved with the introduction of a logger trait, together with a #[cgp_auto_log] helper to implement loggers in similar ways as #[cgp_auto_getter] and #[cgp_auto_error].
CanLog Trait
Before introducing the logging framework, we need to first design a logger trait, such as:
#[cgp_component(Logger)]
pub trait CanLog<Detail> {
fn log(&self, detail: Detail);
}Up until now, we haven't introduced such trait, because it is not yet clear what the exact logging interface should be. If CGP introduce a sub-optimal interface now, it may be costly to introduce breaking changes later on. However, since logging is very fundamental to almost all Rust applications, it is essential that we support the interface through the cgp core crate, rather than providing it through an extra crate.
Log Level
The main challenge with a general logging interface is that it is not clear how we should handle extra logging details, such as log level. One idea is that we would introduce a HasLogLevel trait, and provide the log level through it:
pub trait HasLogLevel {
type LogLevel;
}With that, a Detail type could supply a static log level, such as:
pub struct GreetDetail { ... }
impl HasLogLevel for GreetDetail {
type LogLevel = LevelInfo;
}The main advantage for this is that the log level is optional, and alternative log level interfaces can be supported through the CanLog trait. Furthermore, this allows the concrete log handler to override the log level when applicable.
Named Logging
Similar to named error handling, we will also provide named logging through a separate CanLogNamed trait:
pub trait CanLogNamed<Name> {
fn log_named<'a>(&self, name: &'a Name, detail: Name::Type)
where
Name: Named<'a, Self>,
;
}This would allow us to hide the trait bounds of the log detail types, and perform static dispatch through UseDelegate idiomatically.
#[cgp_auto_log] Macro
With the CanLog trait defined, we can then define a #[cgp_auto_log] macro that can be used on logger traits such as follows:
#[cgp_auto_log]
pub trait CanLogHello: HasNameType {
#[log_info("Hello, {}!", self.name)]
pub fn log_hello(
&self,
#[use(Display)]
name: &Self::Name,
);
}Behind the scene, the macro should generate constructs such as follows:
pub struct __LogHello<'a, Context>
where
Context: HasNameType,
Context::Name: Display,
{
pub name: &'a Context::Name,
}
impl<'a, Context> HasLogLevel for __LogHello<'a, Context>
where
Context: HasNameType,
Context::Name: Display,
{
type LogLevel = LevelInfo;
}
pub struct LogHello;
impl<'a, Context> Named<'a, Context> for LogHello
where
Context: HasNameType,
Context::Name: Display,
{
type Type = __LogHello<'a, Context>;
}
impl<Context> CanLogHello for Context
where
Context: CanLogNamed<LogHello> + HasNameType,
Context::Name: Display,
{
fn log_hello(&self, name: &Self::Name) {
self.log_named(&LogHello, __LogHello {
context: self,
name,
})
}
}Embedded Logging in #[cgp_impl]
Similar to error handling, we should support embedding logging methods inside #[cgp_impl] and #[cgp_auto_impl], so that users can perform logging without requiring additional traits. For example:
#[cgp_impl(GreetHelloWithLog)]
impl Greeter
{
#[use_field(name)]
type Name: Display;
#[getter]
fn name(&self) -> &Self::Name;
#[log_info("Hello, {}!", self.name)]
fn log_hello(&self, &Self::Name);
fn greet(&self) {
self.log_hello(self.name());
}
}Why support abstract logging natively
There is a risk of this bloating the implementation of #[cgp_impl], and make it too complicated. However, given that the pattern for logging is similar enough to error handling, and that logging is common enough in advanced Rust applications, it is essential for CGP to make logging as easy as possible.
We should also consider the main threat of CGP not including logging in #[cgp_impl]: Doing so could cause developers to fall back to using tracing everywhere, and cause CGP applications to continue overly coupled with tracing. One of the end game of CGP to make it possible to write fully-abstract applications that have zero dependency to concrete libraries.
However, the main libraries that stand in the way are logging and error handling such as tracing, anyhow, and thiserror. In particular, it is virtually impossible to have good logging support in Rust without tracing. So it is important for CGP to be able to break this monopoly, and open the pathway for alternative logging implementations to arise through modular logging.