-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
key_value capable log macros #343
Comments
Just an idea for alternative syntax: adding bracket around the key value pairs: info(target: "some target", "some message: {}", msg_arg, {
// And the one of the following.
key1: value1,
key1 = value1,
key1 => value1,
}) Hopefully something like this will aid in formatting long lines, although I'm not sure how rustfmt would handle this. |
I think our only real constraint is that existing I was thinking about capturing key-value pairs implicitly from the format, so if they appear in the message then they appear as key-value pairs: info!("connected to {service} at {uri}", service, uri); however that could be a breaking change and would require procedural macro support. There are other things the macros could do, like allow you to capture a |
I've been thinking about this for a bit, and I think I like the direction @Thomasdezeeuw's proposed quite a lot. Specifically using Exampleinfo!("hello {}", "cats", {
cat_1: "chashu",
cat_2: "nori",
}); Something I particularly like about this is that it feels similar to the way we initialize structs right now. Except they're anonymous, and placed in-line. Also I think the key-values should only be valid as the final argument to log macro invocations. I've been taking a closer look at existing alternatives lately, and it seems that at least some implementations allow key-values to be passed in multiple positions which has felt somewhat confusing to me. Argument Detection@KodrAus I've been thinking a about what you've proposed for a bit, and a problem I see is that while it makes it convenient to add kv arguments to the messages, it becomes problematic when certain arguments should not appear as key-value arguments. I think it slightly boils down to a question of control. By manually passing arguments as key-value we optimize for control, at the expense of some convenience. But given how universal and flexible the Though I'm keen to hear more about the automatic error capturing in macros? What would do you think that should look like? Are there perhaps experiments we could do with this? Implementation@Thomasdezeeuw would you be interested in writing a prototype of the key-value enabled log macros? If you don't have the time, perhaps @KodrAus or I could also take a shot at it, but it might perhaps be a bit longer (I'm for sure running at capacity right now). I'm very excited about this overall; I've been using this feature in a few projects (including gotten a logger running in the browser w/ wasm) and it's been great! Thanks! |
@yoshuawuyts I'm not great at macros, but I'm willing to give a shot. |
I didn't get very far, see #346, I couldn't fix the ambiguity error. Maybe someone with a bit more Rust macro powers can? |
Here is a proof of concept of @yoshuawuyts's preferred syntax as a tt muncher: #[macro_export]
macro_rules! info {
// info!("...")
($e:expr) => {
$crate::info_impl!(($e));
};
// info!("...", args...)
($e:expr, $($rest:tt)*) => {
$crate::info_impl!(($e) $($rest)*);
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! info_impl {
// End of macro input
(($($e:expr),*)) => {
println!("msg={:?}", format!($($e),*));
};
// Trailing k-v pairs containing no trailing comma
(($($e:expr),*) { $($key:ident : $value:expr),* }) => {
println!("msg={:?}", format!($($e),*));
$(
println!(" {}={:?}", stringify!($key), $value);
)*
};
// Trailing k-v pairs with trailing comma
(($($e:expr),*) { $($key:ident : $value:expr,)* }) => {
$crate::info_impl!(($($e),*) { $($key : $value),* });
};
// Last expression arg with no trailing comma
(($($e:expr),*) $arg:expr) => {
$crate::info_impl!(($($e,)* $arg));
};
// Expression arg
(($($e:expr),*) $arg:expr, $($rest:tt)*) => {
$crate::info_impl!(($($e,)* $arg) $($rest)*);
};
}
fn main() {
info!("hello");
info!("hello",);
info!("hello {}", "cats");
info!("hello {}", "cats",);
info!("hello {}", "cats", {
cat_1: "chashu",
cat_2: "nori",
});
} |
@dtolnay that is awesome! Is this the format we want to use? |
Thanks @dtolnay! @Thomasdezeeuw for the |
Future extensions I think we'll likely want:
|
A few additional things to consider:
|
Yay, got a full impl working based on @dtolnay's parser, @Thomasdezeeuw's API proposal, and @hawkw's pointers on how the The impl is modeled after Usageuse kv_log_macro::info;
fn main() {
femme::start(log::LevelFilter::Info).unwrap();
info!("hello");
info!("hello",);
info!("hello {}", "cats");
info!("hello {}", "cats",);
info!("hello {}", "cats", {
cat_1: "chashu",
cat_2: "nori",
});
} Screenshot |
Took a stab at updating the macro to work with named args, but no luck. I think there's something subtle about matching on something as an expression, and then trying to match on it as a token. Expanding fn with_named_args() {
let cats = "cats";
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api_log(
::core::fmt::Arguments::new_v1(
&["hello "],
&match (&(cats = cats),) {
(arg0,) => [],
},
),
lvl,
&("macros", "macros", "tests/macros.rs", 33u32),
None,
);
}
};
// skipping others
} And expanding on fn with_named_args() {
let cats = "cats";
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api_log(
::core::fmt::Arguments::new_v1(
&["hello "],
&match (&cats,) {
(arg0,) => [::core::fmt::ArgumentV1::new(
arg0,
::core::fmt::Display::fmt,
)],
},
),
lvl,
&("macros", "macros", "tests/macros.rs", 33u32),
);
}
};
// skipping others
} The entire named argument expression is being matched on, as opposed to just the actual value. Not sure what to make of that, but I assume it's because matching on If I add a stringify call, it looks like format_args is being called the same in both contexts. fn with_named_args() {
let cats = "cats";
{
let lvl = ::log::Level::Info;
let _as_string1 = "__log_format_args ! (\"hello {cats}\", cats = cats)";
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api_log(
::core::fmt::Arguments::new_v1(
&["hello "],
&match (&(cats = cats),) {
(arg0,) => [],
},
),
lvl,
&("macros", "macros", "tests/macros.rs", 33u32),
None,
);
}
};
}
fn with_named_args() {
let cats = "cats";
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
let _as_string = "__log_format_args ! (\"hello {cats}\", cats = cats)";
::log::__private_api_log(
::core::fmt::Arguments::new_v1(
&["hello "],
&match (&cats,) {
(arg0,) => [::core::fmt::ArgumentV1::new(
arg0,
::core::fmt::Display::fmt,
)],
},
),
lvl,
&("macros", "macros", "tests/macros.rs", 33u32),
);
}
};
} |
We’ve taken another look at this in #461. The syntax is a strict addition to the existing macros to try avoid some of the pitfalls of forwarding directly to |
The new syntax uses a |
cc @rust-lang/libs it would be great to get some input on where things will be at with key-value support in the GoalWork towards a consistent PlanCurrently, the If you take a statement like this: eprintln!("Something happened! {:?}", err); You can reroute that to a different target using error!("Something happened! {:?}", err); This works for any level of complexity in the format args: error!("Something happened! {err:?}", err = e); One of the benefits of replacing error!(target: "my_target", "Something happened! {err:?}", err = e); This is the first inconsistency in the macro syntax. The error!("Something happened at {target}", target = "my_target"); then we wouldn't ever see We can't change the assignment syntax because that's what the underlying error!(target = "my_target"; "Something happened at {target}", target = "my_target"); Now we have two problems:
Solving the first problem could be done in a number of ways:
At this stage, I'd be inclined towards the last point. So instead of writing: error!("Something happened! {err:?}", err = e); You would write: error!(err = e; "Something happened!"); Or possibly: error!(err = e; "Something happened! {e}"); Solving the second problem could be done by changing the way the let target = kvs.get("target").and_then(Value::to_borrowed_str).unwrap_or(module_target); Using a tt-muncher, we could avoid this work unless we spot a Taken together, let's consider a complete example that also includes some other pieces of standard context: error!(target = "my_app", correlation = this_request(), timestamp = now(), error = e; "The request timed out, so couldn't be completed"); In this example, just using key-value pairs in the same way as error!(target = "my_app", correlation = this_request(), timestamp = now(), error = e; "The request timed out, so couldn't be completed {}", e); For what it’s worth, I’ve also got a bit of a squirrel project that reimagines structured logging macros from the start without considering the existing |
There's more we can do with these macros, but since we've shipped some key-value support in |
Continuation of #328 (comment).
Description
kv_unstable
is available inlog
and provides akey_values
field on theRecord
to capture key-value pairs. However currently in order to write data to loggersRecord
types need to be constructed manually, rather than being able to use the log macros. As per RFC#296 we should experiment with logging macros out of tree, and use it to inform how to add key-value logging to log's macros.Constraints
[tbi]
@KodrAus could you perhaps clarify what the constraints are?
Prior Art
Slog
The slog crate uses
;"key" => "value"
pairs in the macros:Tracing
tracing supports two kinds of log styles: "a message + key-value pairs", and "only key-value pairs":
Update this section with more examples
Creating a manual record
For reference, this is how to create a manual record with key-value pairs:
Conclusion
We should experiment with logging macros that can capture key-value pairs and report back with our findings in this issue. It'd be great if we could discuss their tradeoffs, and eventually come up with a design that's suitable for an RFC.
The text was updated successfully, but these errors were encountered: