Skip to content

Support modular logging with #[cgp_auto_log] #185

@soareschen

Description

@soareschen

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions