Build out initial key values API #324
No reviewers
Labels
No labels
bug
duplicate
easy
enhancement
help wanted
invalid
question
rfc
structured-logging
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-rust-lang/log!324
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/kv_unstable"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
For #149
Introduces an initial structured logging API that's feature gated by the opt-in
kv_unstable. APIs behind this feature gate aren't considered stable and may get broken between patch releases that happen to include them.This implementation requires rustc
1.21.0for making it nicer to treat a closure as a fn pointer, and for making it nicer to capture a constant local with a lifetime that's wider than its local scope. Since the feature gate is opt-in we aren't going to break anyone currently on compilers before1.21.0. If we want to make this API available by default then we'll have to bump to1.21.0(which seems ok to me).There is a lot of new code in here, so I appreciate that reviews might take time and it could be a lot of back-and-forth before we're happy. The core pieces are:
trait Source: A source for key-value pairs. Like a repeatable iterator for key-value pairs.trait Visitor: The mutable part of the repeatable iterator for key-value pairs. It's driven by aSource.struct Key: The key in a key-value pair. A container around a string that gives us room to abstract over ownership and add an optional index.struct Value: The value in a key-value pair. A container around an erased fragment of data where its structure is retained by an internal serialization contract. Right now you can only format a value using its debug implementation, but the machinery is there for structured backends (I ended up needing it to implementimpl<T: ToValue> ToValue for Option<T>).struct KeyValueError: An error working with a key-value pair that lets us propagate errors serializing a key-value pair naturally. It's expected that consumers will be producing these.The implementation is based on the structured logging RFC with a key difference:
The
Sourcetrait is object-safe. That means we can't add convenient non-object-safe methods likefor_eachto it even though I've found those super useful in practice and I think we should add them eventually. MakingSourceobject-safe saves some clumsy machinery though and we can work around providing non-object-safe methods:getandto_ownedcan be made object-safe. For instance,getcould be changed fromfn get<Q: ToKey>(&self, key: Q) -> Option<Value>tofn get(&self, key: Key) -> Option<Value>.for_eachandas_mapcan be added as inherent impls ondyn Sourceand/or to an extension trait.chainandfiltercan be added by an extension trait.Once we've merged I'd like to work on support in
env_loggerand integration withslogandtokio-traceand use those as drivers for the next round.r? @sfackler
Note: CI won't pass yet, I have to tweak it to only run on1.21.0+for the new structured API.Also missing in this first round:
Sourcetrait impls for things likeBTreeMaps.ToValuetrait impls for things likeSocketAddrs.pubtypes in this module are currentlypub(super)in visibility.r? @dtolnay
Sorry, I don't mean to put you on the spot to feel compelled to invest your time into reviewing this large new API. If you don't have the bandwidth/interest that's no problem :) It'd be great to at least get a sanity check from somebody on this new code since we the Libs team will be supporting it at some point.
If we'd like to try minimise the amount of bandwidth needed from the wider Libs team to flesh this feature out I'm also ok to continue landing stuff behind this unstable feature-gate and defer a thorough review to a point where everything is implemented and there's experimental integration from structured frameworks in the ecosystem.
An alternative here could be to make this error like
fmt::Error, so it doesn't carry any context on its own and is easy to create.I would be happy to review but I just got home from 2 weeks of travel and have a substantial backlog. Will aim to get you some feedback within the next 14 days.
@ -10,0 +13,4 @@- cargo test --verbose --features "kv_unstable std"- rust: stablescript:- rustup target add thumbv6m-none-eabiI ended up rolling in this tweak to our CI so we do the embedded build as a separate task like the
kv_unstableone, but it does mean we only check it on stable instead of also on beta and nightly. I can revert this change if we don't like it.I haven't gotten a chance to look at key_values/value/*.rs at all yet, but here are a few comments on the rest. My first impression is that this seems promising and very nicely put together! I would be on board with landing something like this.
Returning None is the default behavior so we can leave out this method.
I don't like doing best-effort implementations of Error::description. It is deprecated and people should learn not to use it. We need to keep the method here for now to support old compilers, but I would prefer to return some not particularly meaningful string like
"key_value error"instead.Could you see whether you can set your editor to end every line with a newline? Without that, this isn't a "text file" as defined by POSIX and behaves badly with some cli commands.
https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file
https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline
Would it make sense to provide
impl ToKey for strinstead?Spelling: conversion
You wrote:
And in the RFC I see:
I know this isn't designed yet but is there anything you would be able to describe about what use cases you would like for the index to enable and roughly what sort of code would they want to write using indices?
I count 7 public things under key_values:
KeyValueKeyValueErrorSourceVisitorToKeyToValueI don't think organizing these 7 things across 4 modules (key_values, key_values::key, key_values::value, key_values::source) with half of them present in 3 of the 4 modules, and 14 total import paths, is valuable. I think it would be sufficient to expose all 7 from a single module.
Is this structured under the expectation that this API will grow much larger? If so, I would still like to avoid the case of the same types repeated in 3 modules. What are some types that we might later consider adding here?
Currently the public paths are:
key_values::Keykey_values::key::Keykey_values::source::Keykey_values::Valuekey_values::value::Valuekey_values::source::Valuekey_values::KeyValueErrorkey_values::value::KeyValueErrorkey_values::source::KeyValueErrorkey_values::Sourcekey_values::source::Sourcekey_values::source::Visitorkey_values::key::ToKeykey_values::value::ToValue@ -307,6 +307,9 @@ use std::sync::atomic::ATOMIC_USIZE_INIT;mod macros;Alternative name: mod kv. Do you think this is recognizable and unique and memorable enough?
@ -725,6 +728,37 @@ pub struct Record<'a> {module_path: Option<&'a str>,It would be great to visit the Source here and format the pairs into the Debug output.
Please add a comment that the reason this wrapper exists is that putting
key_values: &Sourceinside Record would prevent using derive(Debug).For the extremely common case of no key-values, I wonder whether there would be a measurable performance difference from using a (private) Source impl whose visit fn does nothing, unlike Option's which needs to check for Some every time.
This type looks like "&mut dyn Backend, but with extra helpers". It sounds like we'll want Backend in the public API eventually, so consider putting the helpers directly on dyn Backend instead:
We can seal the Backend trait for now if we'd like to be free to add mandatory methods.
This seems sketchy as the Rust calling convention for those two function types might be arbitrarily different. I think this is in the realm of "probably works but who knows". It would be good to have a compiler person take a look.
fn(FromAny, &T) -> Result<(), KeyValueError>fn(FromAny, &Void) -> Result<(), KeyValueError>Making them something like
unsafe extern "C" fn(FromAny, *const T) -> Result<(), KeyValueError>seems like it would be sufficient to avoid undefined behavior but I don't know how much worse that makes the rest of the API.This is equivalent to:
I think it would be clearer to just write that, rather than leaving it up to how Option::None::<()> gets formatted.
So this all came out of a possible
Source::get(Key) -> Option<Value>method, where for sources like aBTreeMap<String, Value>the implementation could do an indexed lookup, but forVec<(String, Value)>it would need to scan. It could be a useful thing for a logging framework that wrapslogand has some well-known structured pairs like a timestamp or correlation id that consumers want to poke at.How you would get a hold of an index, and how useful that index would be if you, say, concat two
Sources together is where I stopped the design train and left it as a possible extension. For the well-known pairs use-case you could also just write aSourcelike:All that's to say it's something I didn't want to absolutely rule out as an enhancement but it's still airy-fairy.
@ -307,6 +307,9 @@ use std::sync::atomic::ATOMIC_USIZE_INIT;mod macros;I think I was using the name
kvat one point, and only switched it tokey_valuesso the module name was the same as theRecord::key_valuesmethod. When I first starting exploringslog, I found its abbreviated names hard to follow, but those were type names not module names.I'm on board with
kv.This sounds like a good excuse to bootstrap some micro-benchmarks for the
logcrate, so I'll come back to this one later!Yeh, this was the weird API I was hoping to get some more eyes on at the All Hands earlier this year.
It's the same approach that
std::fmt::Argumentuses, but that's unstable. It's also similar to thestd::task::RawWakerVTablewhich is up for stabilization now, but includes some of the extra type safety @eddyb suggested in the stabilization PR. The impression I've gotten is that this approach is ok, but is well off the beaten track and not something you should reach for if you don't have to.As for why we need it, there's a case where you're bridging between
logand another framework where you effectively need:If your bridging crate doesn't own the type
Tand it doesn't satisfy either of those bounds on its own then you're stuck. You can't wrap&'kv Tin a newtype because you need the data withinValueto live for'kv. That's whereValue::from_anycomes in. It lets you create aValue<'kv>from any arbitrary&'kv T, regardless of the traitsTimplements.Additionally, when we come to integrating other serialization frameworks like
serdeintolog, we can do it by erasing into the sameAnymechanism, so there's no matching required.This approach comes with drawbacks though:
from_anypublic, I can't think of any alternative internal implementation ofValuethat could support that&'kv T -> Value<'kv>method.This PR doesn't make the
from_anymethod public though, so right now there's no reason to use this erased approach over an internal trait object. I just copied it from my reference implementation. For implementation simplicity I'd be on board with replacing this with the more obvious trait-object-based implementation.The idea behind the
FromAnytype (and the rest of theanyAPI here) is to try really hard not to makeValuelook like a serialization contract. The reason is mostly philosophical. The exercise of building out thesvalframework convinced me that building a serialization API is tricky, and design decisions can seem arbitrary but have significant impacts on usability and interoperability (svalhas some really wacky code to integrate withserde), sologshould avoid owning that story and defer to existing libraries.As an example, the way I imagine we'd add
serdesupport toValuewould be:Internally, we can't get away with not having some contract for frameworks to conform to, but we can avoid ever exposing it directly. Instead, we can implement traits like
std::fmt::Debug,serde::Serialize, andsval::Valueonlog::Valueas the way you can inspect the underlying structure instead of implementing a trait.But as someone with a lot of experience working on serialization APIs I'd welcome any input you've got here!
Thanks @dtolnay for a thorough review! I'm working through your feedback and have left some of my thoughts on the
valueAPI. I appreciate the fresh perspective.Alrighty, in https://github.com/rust-lang-nursery/log/pull/324/commits/ee05032759cf2fd6c04b6501b8c35b8e2e3942a7 I've refactored this to remove this
FromAnyinfrastructure and use a pair of internal traitsVisitandVisitorto preserve the structure of primitive values.@dtolnay Alrighty, this should be ready for another look now.
@KodrAus Quick note: I'm missing
ToValueimplementations forusizeandisize. More feedback to follow, looking good so far!I've added support for this to my logging implementation to test this pr, it's available here: https://github.com/Thomasdezeeuw/std-logger/pull/14.
As mentioned in a previous comment. I'm missing
ToValueforusizeandisize.It would be nice to detect if the record has any key value pairs, this would avoid needless
formatting. Could be as simple as changing
key_valuesto returnOption<&dyn Source>.Could
KeyValueErrorbe renamed tokv::Error(where thekvis the module), this would match types likefmt::Errorandio::Error. This does mean that without the module name in front of it the name loses all meaning.I'm missing a
Value::from_displaymethod that uses thefmt::Displayimplementation in printing/formatting. I've also noticed that thefmt::Displayimplementation forValueactually uses thefmt::Debugimplementation viaFmtVisitor.I'm also missing a lot of docs. For example for the
kvmodule: what the types and traits and how they work (together). But this can be future work.Thanks for the feedback @Thomasdezeeuw!
We should definitely add these 👍
We could look at a convenience method on
Sourcethat can tell if there are any pairs present so you could avoid formatting. I think it would be good to avoid exposing aOption<&dyn Source>though so we avoid the confusion ofNonevsSome(&[]).That's the name I'd prefer too, but used
KeyValueErrorinstead so that it's consistent with the explicitly named errors that already exist in thelogcrate. I'd happily change it tokv::Error, and maybe consider re-exporting it aslog::KeyValueErrorat the crate root (or not).That's a good idea too!
This actually shouldn't be an issue, because we can forward a
fmt::Displayimpl through afmt::Debugone, so whateverfmttrait you use for a particular value is the one that'll be used in both theValue as DebugandValue as Displayimpls.I'm not sure I understand, when do we want to use
Some(&[])? And what confusion would it cause?If the
key_valuesfield inRecordis changed toOption<&dyn Source>we can just return that right? ThenRecordBuilder, and any log messages with key/values, can just default to usingNone.Ah sorry that wasn’t very clear. The confusion is assuming that if
key_valuesreturns some then there are more than zero pairs to work with. That’s not necessarily going to be the case, so having to jump through the extra hoops of matching on anOptiondoesn’t seem worth it.Nice, this looks terrific.
🎉
@Thomasdezeeuw The intention now is that we can continue developing off this base without having to break it, but you never know. So in case that does need to happen I'll try remember to give you a CC in any PRs I see that would result in breakage!
Any more input you have in the form of issues and PRs will be most welcome! I've created a tracking issue we can use to follow progress.
But we could ensure that when
key_valuesis some there are one or more key values. Then we won't have that problem.