Tracking issue for kv_unstable (structured logging) #328
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#328
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Tracking issue for the implementation of #296. See also #149
slogslog::Valueandlog::kv::Value. We should be able to do zero-allocation conversion fromlogtoslog, but may need to allocate fromslogtolog.tracing(https://github.com/tokio-rs/tracing/issues/341)tracing::Valueandlog::kv::Valuein theevent!macro (#339)std::error::Errors asValuesfernasync-logstd-logger(https://github.com/Thomasdezeeuw/std-logger/pull/14)Sourcehas any key-value pairs (#329)femmeenv_logger(https://github.com/sebasmagri/env_logger/pull/137)antlog(an experimental wrapper to prove out requirements)log!("msg {}", interpolated, { key: value })(#353) - We ended up having to revert this inlog, but are experimenting externallylog_kv_macrolog_macrosserdeserde::Serializeandstd::fmt::Debug(https://github.com/rust-lang/rfcs/pull/2696). Seeserde_fmt.sval(#342)Source::countreturn aResult<usize>instead of a plainusize?@KodrAus thanks for all the work you've put into this (and linking to this issue from a forum post).
I was curious: is there a release planned that includes the beta version of
kv? I actually wanted to use the beta forasync-log, but given there wasn't any release yet it wasn't possible 😅Hi @yoshuawuyts! 👋
Right now any release of
logwe publish will include this API under thekv_unstablefeature gate, but we're not ready to commit to backwards compatibility just yet. So if you depend onlogfromgitthen you can access the API:I think what we need are a few draft PRs in some of these log frameworks that tease out any breaking changes we should make, and then will be able to publish a release that replaces the
kv_unstablefeature gate with akvone.Oh oops, maybe I was being a bit vague. The last release on crates.io was done in October 2018, which means currently no releases ship with
kv_unstableon crates.io. Which in turn (I believe) means we can't ship any versions to crates.io.In order to help test this feature, it would be nice if there was a release on crates.io with the
["kv_unstable"]feature available. For experimental technology such asasync-logI think we can get away with publishing a version that directly depends on this feature, and keep updating it as it's polished.So my question is: could a version be of
logbe published to crates.io that includes the experimental flag?P.S. Thanks for taking time out of your day to reply to my questions!
Ah this sounds fair enough. I think we can get pretty far without needing to break anything, but it's probably going to happen at some point. But right now it is harder for folks to get started with these new APIs, so once #339 lands I'll put together a release so you can depend on it from
crates.ioand browse the docs ondocs.rs.@KodrAus in the top-level post there's an entry about "Explore macro support". Do you have any plans to experiment with this?
We've integrated key-value logging in https://github.com/lrlna/femme/pull/1; all that's missing now is a way to create key-value pairs and send them to a logger. Even a small code example would be helpful, but ideally there'd be a small log crate that could work with this.
Also: is it perhaps useful to open a separate issue to track/discuss logging macros?
edit 2019-07-21:
Ah, I think I figured out how to log a kv-pair!
Ah yes that
Record::key_valuesmethod is the way you can attach key-value pairs to a record in the absence of macro support. It needs to be a&dyn Source.Opening a separate issue to design macro support sounds like a good idea! We’ve got a couple of constraints but also plenty of opportunity to improve things.
Macro issue created: https://github.com/rust-lang-nursery/log/issues/343
Created a crate for shared types in key-value logs (to mark pairs of logs) -- https://github.com/yoshuawuyts/log-types. Not sure yet how useful this is, but I think it might be a fun reference of how to create source/key/value structs, and probably a useful experiment (:
Having some standards around well-known key-value pairs is something I've also been exploring in a little wrapper. It seems like it'll be useful for a more composable logging ecosystem.
@KodrAus Yeah for sure! -- I wonder which others to add; I only really thought of
spans::{start,end}andhttp::{request,response}. Though I guess timestamps, host, ip might be common too?Oh also, I saw femme is not listed yet; could we perhaps add it to the tracking issue?
I'm hesitant to add to the list of todos because I really want this feature, but it might be worth adding fern as another crate to integrate with. I don't use it (I use slog), but it seems like it has quite a lot of usage (600,000+ all time downloads on crates.io).
Rationale is it looks more reasonable to just make it a type-system question whether a certain type has a notion of a count of it's
kvs or not.And if that's not the desired use case for making
countreturnResult<usize>- then what is? What do you see as possible error values? Why not justOption<usize>? Some lazy-evaluatedkvsources might not have count known upfront, i.e. something like processes list (ps ax) might be collected on demand.@MOZGIII unfortunately a separate trait wouldn't really work because we'd lose the implementation when erasing as
dyn Source.The desire for
Resultwould just be to be able to pass through any errors from the default implementation that calls the falliblevisit_pairmethod. ButOptionseems worthwhile!Hmm, I don't think the current implementation really supports this kind of scenario because pairs can be visited and fetched at any point. I also don't think it's really desirable to support either, because pairs may be inspected and visited multiple times any expensive work or externally changing information could be confusing to end consumers.
Given all that, I'm having hard time imagining a case when the
countwould beNone(orErr). Do you have an example in mind?How is the
countused? Is it like iterator'ssize_hint?From how the code is set up, it looks fine: the
Sourcecan be used at any time to collect values viaSource::visit. There is an implementation note on visit fnA source should yield the same key-value pairs to a subsequent visitor unless that visitor itself fails.that advocates against sources being dynamic. After looking around the documentation more, I found the uses forSource, and it makes a lot of sense for it to be required to not be dynamic. 👍I've spent a little time looking at how we could support attempting to coerce structured values to concrete types. This is something that would make logging frameworks that want to enrich
log::Records with their own well-known types nicer. There's a PR in #379Once we have that and #378, we can add support for
std::error::Errorto structured values too. It might look something like:I see there's a 'kv_unstable' feature flag in the latest version of the
logcrate but it's unclear how to interface with in from an application. Is this not something we can do today?I found this commit which seems like it reverted an interface users could leverage is https://docs.rs/kv-log-macro the crate users should be guided towards?
@softprops yeah, we're still in the process of figuring out the macros. Currently
kv-log-macrois one of the most convenient ways to use the kv feature, but the goal is to eventually have this functionality inlog's macros itself.Have we figured out other ways to preserve states? The current
log!("", { key: value })will be quite duplicated when the same{ key: value }needs to be reused in different scopes.@yoshuawuyts and I discussed the path to stabilization for the kv support and came to the conclusion that there's really not much blocking calling the
kvmodule stable and continuing to work on it without breakage. I think we've explored enough of the design space to cover macro support and integration withtracingproperly.What about macros?
We don't currently have macro support in
logitself, but since we're bound tostd::fmtthe macros we can provide in here in a backwards compatible way can't be as capable as what we produce externally. We will still provide macro support inlogitself, but that doesn't need to block removing thekv_unstablefeature gate.Right now, if you want macro support you can check out
kv_log_macro. It's an almost drop-in replacement for the existing macros.I'm also experimenting with something completely greenfield for macros that I hope to talk about more when it's fleshed out.
So what's left?
I'd like to get #396 merged, which lets us capture values using a trait bound like
impl Display + 'staticbut still determine at compile-time whether the actual value is one of a number of primitives likei32. Until 1.46.0 is released in August it'll have a small runtime hit though. I think that's the last breaking behavioural change we need to make then can do a thorough API review.What's after stabilization?
@yoshuawuyts brought up the possibility of adding logging/tracing support into
std, which I think is worth exploring. I've been framing it as a structured/contextual enhancement tostd::fmtbut going down that route might be the best possible way to make the existing log macros natively structured.What's the story for the kv visitor trait and serde. It seems today the visit api gives you a access to a Key and Value. You can get the
&strvalue of aKeybut values are a bit opaque. I know that they implementDisplayandDebugbut i'd rather depend on their serde::Serialize impl when serializing them out.Hey @softprops 👋
It would be pretty unfortunate for structured values to only support the unstructured
DebugorDisplayoutputs. I wrote a bit aboutserdein https://github.com/rust-lang/log/issues/388#issuecomment-629572564.The gist of it is we currently support
serdein a roundabout way using a different structured framework, but should definitely add first-classserdesupport usingerased-serde(we could fall back to the alternative framework whenallocisn’t available). There’s nothing blocking this and it can be implemented at any time.@KodrAus that's awesome! Is anyone actively working on this? I'd love to help but am not caught up on all the current and prior discussion.
@softprops As far as I know it’s not something being actively worked on yet. If you’d like to take a look that would be great! Otherwise it was next on my list after #396, since that changes the shape of the value internals a bit.
To add
serdesupport we’ll effectively add the following public API:The things we’d need to do internally would be:
erased-serdeandserde-fmtkv_unstable_serdefeature that pulls in thestdfeature,sval/serde,serde-fmtandserdekv::value::internal::serdemodule, probably following a similar structure tointernal::svalkv_unstable_svalandkv_unstable_serdeare available, we can usesval‘sserdebridge to implement theVisitor::svalmethod in theserdemodule and theVisitor::serdemethod in thesvalmoduleserde-fmtto implement theVisitor::debugandVisitor::displaymethods in theserdemoduleSerdevariant tointernal::Innerserdemethod tointernal::Visitorkv_unstable_serdeto our GitHub Actions on its own and combined withkv_unstable_svalBut if anybody waned to dig in I’d be happy to help! The best place to reach out would probably be on Zulip.
This sounds great. I'll be sure to drop a note here if I start anything.
I had some issues getting 0.4.11 structured logging working. I've written my own logging macros because I have some specific requirements, and I'm also writing my own Logger to receive all this data because the outgoing logging channels I have to interface to are custom. I chose to put it all through log crate because I also want to capture unstructured logging. Some observations/questions:
to_*methods in turn on the Value to find what type it contains, so wouldn't it be better to turn that around and have several different methods on the Visitor which would be called according to the type of the value? e.g. visit_pair_u64, visit_pair_str, etc. The types handled by OpenTelemetry for example are limited toi64,bool,f64andstr, plus arrays and tables. So ignoring the nested values for the moment, and addingu64, that requires just five visitor methods to cover it.ValueimplementsDisplay, because for text-log output, I started out by querying the differentto_*methods and formatting for each one. However this didn't work because I couldn't find a way to get a value created withValue::from_displayout using theto_*methods. So maybe it would be best to have twovisitcalls, one for text-style output, which just takes a FnMut and gives it a&strfor each value (which would point to either the original value or else an internal temporary format buffer), and another for getting binary data out, which has a Visitor with multiple entry-points (for i64/u64/f64/bool/str/etc). I haven't implemented binary output yet, but it seems like right now I'll have to try variousto_*methods in turn, and then finally try aformat!("{}", val). This doesn't seem very efficient. (The log crate is advertised as "lightweight".)I was trying to understand the current code. There is a lot of dynamic dispatch going on, and a lot of type machinery. Is all this necessary? If it is, then fine. However did you consider something like a HCons-style approach? i.e. construct a nested type which would result in all the heterogenous keys/values being stored sequentially in stack memory with minimal padding (e.g.
KvInt(("key", 123, KvStr(("key2", &strvar, KvEnd))))), and let the compiler build a singlevisitfunction for it all that accepts a Visitor and visits them all (where the Visitor has an entry-point for each fundamental supported type). That would make good use of static knowledge from the callsite to optimise at least the first level of processing. (Nested arrays or tables would need some kind of dynamic approach.) I can provide some prototype code if this isn't clear.Actually, thinking about it some more, would it be possible to make it work with a closure like this:
That way, assuming all the Value-related calls are marked
#[inline]it should all get inlined to produce flat code that just executes a series of calls on thevisitorinstance. This assumes that the Visitor has separate entry points for major types (u64,i64,f64,bool,str). The closure will build a hidden structure on the stack which contains the references to all the variables being logged. So this should be really efficient and lightweight. I could prototype this if it is of any interest.Hey @uazu! 👋
Sorry for taking so long to circle back and thanks for putting together your thoughts 😃
The reason
Visitoris a trait instead of just aFnMutis that it gives us a place to add provided methods likegetandcount, soVisitor::visit_pairis likeIterator::next. You only implement that one method but get all the other combinators on top. For your last example, you can already do this:The goal of the
Value::to_*methods is for more ad-hoc inspection. If you're writing a log sink that wants to do something particular with a timestamp or trace identifier for instance you can check whether the key-value pairs you're given contains a value calledtimestamp, and then check whether that timestamp is a string or achrono::DateTime<Utc>. They're really inconvenient for trying to serialize structured values directly. In order to visit all pairs and serialize them you use either thesval::Valueorserde::Serializeimplementations which have the API you're describing.The kicker for the visitor API is the plus arrays and tables part. Once you have to support complex datastructures the API becomes a lot more complex and the open design space a lot bigger than just simple primitives. Not supporting arrays and maps isn't really an option because you certainly want to log them. So we offer two options in
logfor properly serializing values: there'ssvalfor no-std serialization andserdefor wider compatibility. Both can handle any shaped datastructures and require fairly large APIs to do so.I'd be interested to see a HCons-style approach! My experience with H-lists in Rust has been that they're somewhat going against the grain of the language and so require a lot more type machinery, various hacks, and tend to produce large and incomprehensible error messages. One of the goals of
logas well is to generate as little code in callsites as possible, ideally just a single function call. But that doesn't mean exploring different approaches isn't worthwhile! We settled on trait objects fairly early and never deeply considered trying to go the other way into fully generic code.@KodrAus Thanks for replying.
In my opinion, since 'log' is supposed to be a lightweight logging crate, the key-value logging mechanism should be lightweight too. It doesn't need to support everything -- a superset of JSON and OpenTelemetry style values would be enough. It should be streamlined for dumping the data to a stream, since that's the most common operation.
In case it's of interest: My original reason to look into this was to add minimal key-value logging support to my Stakker crate. In the end I implemented my own minimal logging interface directly in Stakker, with the logging macros in a separate crate: stakker_log. This was done because none of the crates out there did quite what I needed. Also because I needed to transparently pass through a span ID (which is effectively an actor number) with the log records, to support span-based logging. Also, since Stakker is single-threaded, it's more efficient to log within the same thread, rather than to something which then has to use a Mutex or whatever. The intention is that the user-provided logging handler could either dump out directly, or forward logging to whatever logging crate the user prefers. This does also handle nested values (arrays and maps), although in a very minimal visitor style: just a closure at the point of logging which dumps the data as calls to a visitor closure which outputs it.
That's absolutely important! One of the balancing acts we're playing in
logis that it needs to be lightweight, but because it exists between other libraries and frameworks it also needs to be permissive enough to retain as much context as it can when dealing with data from other sources. So by default it supports visiting pairs of displayable key-value pairs and additional features let it integrate with more feature-full serialization frameworks. Your thoughts did end up prompting #426, which makes it easier to serialize the complete set of key-value pairs as either a map or list.Ah it's nice to see the kind of API you've been talking about in practice there. Your
LogVisitortrait actually looks pretty similar to what I ended up with insval. That library was actually spiked out withinlogoriginally, but I pulled it out because it become too big to maintain inline. That's the same story withvalue-bag, which is the basis oflog's value API.I've started working on improving the implementation of translating strucuted data between log and slog (and just implementing it to begin with in the slog -> log direction).
For log-> slog, I don't think it can be completely allocation free, because the slog
Keytype internally uses aCow<'static, str>, but the logKeytype can have a narrower lifetime than'static, so it is necessary to allocate new strings for the keys. See https://github.com/slog-rs/stdlog/pull/25/files#diff-2f5dbd891ecf8d768b1b394d2138b2c244bd8dc54ff8ad4b16325877f3709eb7R46For slog-> log, I ran into a more difficult hurdle. I can't directly create a
slog::Serializerthat forwards calls to alog::kv::Visitor, because the lifetimes don't work out. Fine, I can build up a data structure containing the key values, like aVec<(String, Value<'static>)>. And that almost works, unless the value is a string, because there is no implementation ofFrom<String> for Value<'a>. I don't think it is completely unsolveable, I could instead useVec<(String, Box<dyn ToValue>)>, but that then requires boxing everything. Or I could create a newSourcethat contains both aVec<(String, String)>and aVec<(String, Value<'static>)>, but that is more complicated. It would be a lot simpler if there was a way to create aValue<'static>that includes an owned copy of aString. That might require a change in the upstreamvalue_bagcrate.u128 and i128 have a similar issue. Although, in that case falling back to representing them as a string probably isn't terrible.
I imagine most keys being
'static, e.g. when creating using of the macros, so perhaps we can useMaybeStaticStrinternally? Same as what we do for aRecord's module and file? Then we can providefn as_static_str(&self) -> Option<&'static str>. That avoid a good number of allocation in slog I think.I think
kv::Value, and thus thevalue_bagcrate, was designed with borrowing instead of owning value in mind. But I'll defer that to @KodrAus. We could do a simple pointer tagging to indicate an owned string, but that we would have to drop the capacity I think.Does slog not support 128 bit integers? If so, that's a limitation in slog, not so much in log.
Slog does support u128 and i128, but there isn't a way to store them as owned values in
Value, because value_bag just stores a reference to them (I assume to keep the size small... although&stris a fat pointer right, so on 64-bit architectures it would already need 128 bits 🤔 ). I did figure out a way to solve this by making a wrapper type that holds the owned value. See https://github.com/slog-rs/stdlog/pull/26.However, I couldn't figure out a way to preserve a
&dyn (Error +'static), because the lifetime isn't long enough, and there isn't any way to convert to an owned type. To resolve that, there would probably need to be a breaking change in either slog (change it to use references that are tied to theSerializertype, similar to log'sVisitoror add a Clone trait bound to the error type) or log (relax the lifetime).The former would probably be better, since it could allow less allocations in other serializers for slog as well. But I'm not sure that's worth the churn.Any plan to make this stable ? More than 4 years since issue opening. This feature is mandatory for attaching contextual information to logs (user, error id, use case, etc.) that can then be passed as kv to logging/event ingestion systems (ELK, Loki, InfluxDB, etc.).
Or any workaround ?
The workaround is to use the unstable feature, or use the slog crate.
tracingis quite popular and was designed for structured logging. It can catch the (unstructured) log events oflogand turn them intotracingevents. I don't know whether it's more stable than this, though.It is better to have a unified log library. I know that the popular ones are
log,slog, andtracing. It is better to add log to the standard library, likelog/slogin golang.For structured logging, format strings seem less important, for example:
The following one is better (syntax of tracing):
Forgoing format strings makes macros a lot more pleasant to design.
It would be great to have a fresh look at anything blocking this and I can't really think of any left 👍 The macros are a bit rough-and-ready, but I think suitable for most use cases. We really should get this out the door.
Before stabilizing, I think the next step would be running through a full API review with @Thomasdezeeuw, @JohnTitor and anybody else who might want to participate.
Yeh, I think the issue here is format args producing a string and throwing away its interpolated arguments. It's nice and intuitive to be able to both describe your event and some of the structured data attached to it using some kind of interpolation. This practice is popular in .NET for example, but Rust's
format_args!is just geared towards string construction.I can only think of two things that we should think about of the top of my head, but neither should block stabilisation:
as_debug!(so?my_value) and related.Visittrait. Here is for example a concreteVisitimplementation that outputs JSON:github.com/Thomasdezeeuw/std-logger@eff67f4c33/src/format/json.rs (L167), but it doesn't handle serde or sval based values.I can do that next weekend 👍
Since we're really dependant on rust std lib on this you could ask in https://github.com/rust-lang/rust/issues/99012 for this. Though it might conflict with https://github.com/rust-lang/rust/issues/78356 which was stabilised in 1.71.
I think this is something that would be reasonable to do 👍
tracinghas already set a bit of a precedent that would be good to respect.Ah, I think this highlights one thing we definitely need to work on; documentation. You should be able to support
serdeorsvalin yourvisit_anyimplementation. TheValuetype always implementsserde::Serializeorsval::Valuewhen thekv_unstable_serdeorkv_unstable_svalfeatures are enabled. So if you add, say, aserdefeature tostd-loggeryou should be able to just pass theValueyou're given directly toserde_json::to_stringand it'll "just work".Personally, I think it's ok for
format_argsto optimize for string construction. That lets it consider optimizations like pre-formatting constants that it otherwise couldn't do.If you have the bandwidth to run over things sometime that would be amazing! I'd be happy to do something a bit more synchronous too or at least do a pass over our docs if that would be helpful?
I've done a review of the key-value feature in the log crate and opened https://github.com/rust-lang/log/pull/584 to discuss some things. Overall I think it looks pretty good and I think we're close to stabilisation. I still want to review the valuebag crate (added dependency), I think I'm going to skip serde and sval as they are much larger and more widely used already.
I've also opened https://github.com/rust-lang/log/pull/583 containing some small fixes and additions.
Thanks @Thomasdezeeuw!
A good review of
value-bagwould be much appreciated too. It originally started as the infrastructure here inlog, but became a bit heavy so I pulled it out. Its public API is very similar tolog::kv::Value. For the review oflog::kvitself, maybe we can use #584 so discussion isn't split here?@KodrAus I took a look at value-bag (from the perspective of using it in the log crate). Overall I think it looks good 👍
For the
capture_*andfrom_*methods I have the same comment as I made here: https://github.com/rust-lang/log/pull/584#discussion_r1308453837 (so let's continue the discussion about this there).We could get the size down from 3 to 2 words (on 64 bit) by using tagged pointers, but I don't know if that is worth all the
unsafecode.One final note is around the
Filltrait, which isn't used by the log crate, but it was a little surprising to me that theFill::fillmethod seems to be called multiple times on every access (?). That wasn't clear to me by (quickly) scanning theFilldocs. Maybe a note in the docs would be sufficient.Thanks for this work. Looking forward to it being released proper.
Some systems generate a significant amount of log data in debug mode, so it might be worth the effort, but personally I wouldn't want a change at this point to hold up release of this feature. It seems more important that structured logging become a stable feature sooner rather than later.
log is used by over 50000 crates, which means you'll inevitably pull it in as a transitive dependency. I'm therefore concerned about value-bag being added as a dependency here. Will this remain an optional, non-default feature?
I'm afraid not, it's the core of the
Valuetype (about 50% of this feature ;) )Maybe we can move it to the rust-lang org? /cc @KodrAus
@chloekek I think that's a reasonable concern. In fact, in #613 I've made that dependency fully optional for structured logging, so if you just want to log simple primitives, like numbers, strings, booleans, or formattable values, you won't pull it in.
For some background on what that
value-baglibrary is (as well as a serialization framework calledsval), they all started out as the implementation directly here inlog, but grew in complexity to a point where it seemed best to split them out. I didn't originally try move them to a repository in therust-langorganization I think because I didn't want it to carry any sense of being an "official" solution to dynamic values. So I created an organization: https://github.com/sval-rs and it's been living and continuing to evolve there.I would happily move that repository anywhere or add any collaborators of
logto that organization 🙂