-
Notifications
You must be signed in to change notification settings - Fork 161
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
Compile-time source generation for structured log messages #195
Conversation
I would love the 2 ideas for what gets generated in here. |
803588d
to
04e1532
Compare
04e1532
to
cecac51
Compare
proposed/logging-generator.md
Outdated
### Benefits: | ||
|
||
- The name hole approach adds enough value that would be good to consider in the future for cases where we are given an interpolated string rather than message templates. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the comment made back on the original issue stated:
"A benefit there is that we can use string format syntax as a natural way which we can consider for imperative logging, but the consensus there is to keep the logging generator to enable writing more declarative logging APIs."
FWIW, I think this is a strange line being drawn between "declarative" and "imperative". From the developer perspective, we're talking about the difference between (using one of your examples from earlier):
[LoggerMessage(0, LogLevel.Information, "Hello `{name}`")]
public static partial void LogName(this ILogger logger, string name);
and
public static void LogName(this ILogger logger, string name) =>
logger.Log(0, LogLevel.Information, $"Hello `{name}`");
If we define "declarative" as using an attribute, sure, one does and one doesn't. But otherwise, we're still declaring the same information, just slightly different syntax, in one case as arguments to an attribute ctor and in one case to a log method.
The valid reasons I heard that we might prefer the source generator approach were:
- Wanting to enable the exact format syntax required by https://messagetemplates.org/
- Wanting to be able to pass all of original data as-is without any complication around how it's stored prior to something being done with it other than creating a string
- Wanting to be able to supply alternative names for the holes, if the C# 10 syntax doesn't end up allowing that (but Fred is investigating)
None of the other reasons I heard were compelling to me for why we'd go the source generator route.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair. I think they both offer declarative. I will update the proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly don't think these approaches are at odds but:
- The structure has to be preserved (Names), that's the main thing about the string builder approach that is missing (hopefully we can make something work in C# 10).
- Not being able to lazily ToString is a problem, we don't want to allocate until a logger provider asks for the string
- The approach is declarative and toolable. We can write rules against the attribute instead of call sites that use Log.
I say we treat the builder approach as distinct from the source generator. In fact, in the later proposals we're going to generate calls to LoggerMessage.Define<...>
. This change basically re-enforces that the source generator is a declarative approach to the model we have today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The structure has to be preserved (Names), that's the main thing about the string builder approach that is missing
This is my "Wanting to be able to supply alternative names for the holes" bullet. If just {name}
is used, it can already support that. I suggest if specifying names separate from the expression itself is important, that the design document call that out and show it in examples: currently I don't see any examples that supply different names.
Not being able to lazily ToString is a problem, we don't want to allocate until a logger provider asks for the string
This is my "Wanting to be able to pass all of original data as-is without any complication around how it's stored prior to something being done with it other than creating a string" bullet. But it depends on what you mean by allocating. I imagine the builder would either format into an ArrayPool-rented char[] or serialize into an ArrayPool-rented byte[]. And of course only after checking whether logging was enabled at an appropriate logging level.
The approach is declarative and toolable
I still question the declarative statement here. An analyzer would search for attributes and look at attribute arguments, or an analyzer would search for Log method calls and look at method call arguments. Is the statement really that sites are implicitly constrained by what you're allowed to write in attributes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my perspective "declarative" is really about the fact it is a "guided" experience. The code generator has deep semantic understanding of what the user is doing and provides useful error messages that don't exist using completely generic mechanisms.
For example, the code generator emits warnings for duplicate event ids. Or it'll auto-generate a message if you don't supply one to output parameters as a JSON blob, etc.
The generator model also makes it possible to provide parameters to the logging method which aren't reflected in the message string. This lets you pass name/value pairs to the ILogger without necessarily including that state in the summary string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In terms of supplying different names, the current implementation provides a limited form of that as it optionally supports pascal casing of names. So you can supply "foo" as a method parameter, and call it "{Foo}" in the message text (and in the downstream IReadOnlyCollection produced on the TState given to ILogger). This feature is controlled by a global option on the generator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my "Wanting to be able to supply alternative names for the holes" bullet. If just {name} is used, it can already support that. I suggest if specifying names separate from the expression itself is important, that the design document call that out and show it in examples: currently I don't see any examples that supply different names.
We change casing all of the time in ASP.NET Core. We also define templates separately from where we specify values so this happens all the time. The intent isn't to capture variables but to design log messages and their structure and pass the appropriate values after the fact.
This is my "Wanting to be able to pass all of original data as-is without any complication around how it's stored prior to something being done with it other than creating a string" bullet. But it depends on what you mean by allocating. I imagine the builder would either format into an ArrayPool-rented char[] or serialize into an ArrayPool-rented byte[]. And of course only after checking whether logging was enabled at an appropriate logging level.
We need to eagerly stash all of the state before we ToString the overall thing. This makes it hard to avoid boxing or calling ToString on the incoming parameters. So there's an allocation forced up front to produce the builder.
I still question the declarative statement here. An analyzer would search for attributes and look at attribute arguments, or an analyzer would search for Log method calls and look at method call arguments. Is the statement really that sites are implicitly constrained by what you're allowed to write in attributes?
Attributes force a particular pattern that we like and is easier to analyze than code. This is a style preference thing though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to eagerly stash all of the state before we ToString the overall thing. This makes it hard to avoid boxing or calling ToString on the incoming parameters. So there's an allocation forced up front to produce the builder.
If you don't count ArrayPool rentals as allocation (waving my hands), then there's no allocation to create the builder. You're right there would be ToString allocations as part of formatting into that, for any non-primitive types that allocate in their ToString methods. I expect with ISpanFormattable that will go way down, though, since there wouldn't be allocation for anything implementing ISpanFormattable.
e6cf0e3
to
27bf951
Compare
Co-authored-by: Fred Silberberg <frsilb@microsoft.com>
2d3f26d
to
e7b640b
Compare
Could you run |
That's not so friendly on Windows for all developers. I tried to run that command with windows bash.exe and got a bunch of errors around line endings. We should probably come up with a way to make it work regardless of developer platform. |
Ouch. I didn't realize that.
I dont use Windows, but I am happy to help make this happen. Any suggestions on what a Windows-friendly workflow would look like?
Should it just update the file in-place directly? Would that help with the line ending issues? |
Nevermind, I see this is a python script. We can just tell folks to use python to run it. I see a couple path separator issues. I'll put up a fix. |
- Added benefits with source generators not supported with `LoggerMessage.Define` - Moved md file to other folder
- Mention message/level required but event ID could be skipped
Note to reviewers:
|
accepted/2021/logging-generator.md
Outdated
@@ -333,6 +332,162 @@ same console logs formatted using JsonConsole: | |||
} | |||
``` | |||
|
|||
### Added benefits with source generators not supported with `LoggerMessage.Define` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really understand this. This feels like a choice. We chose not to add those overloads. If we want to support more than 6 overloads then we need to add those overloads...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even if there were overloads up to taking 16 parameter (Action limit). then the source generator would still be able to fallback to another approach to support arbitrary number of parameters
I think with this section I was trying to illustrate that source generator can deal with some of the existing limitations of Define
, one way or another, if we wanted to allow more robust support. (arbitrary number of parameter is one of the several items of limitations explained in this section)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are recommending to switch that ability off for preview 4, to only allow up to 6 parameters until we find request for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are recommending to switch that ability off for preview 4, to only allow up to 6 parameters until we find request for it?
I think so. I'm not sure why the API was rejected TBH. Maybe we're waiting until customers complain 😄 (which they have in the past)
@maryamariyan time to merge this? |
Remove arbitrary number of params support
It should be ready now |
678dffb
to
ddeecdb
Compare
I gathered all my notes into this design doc. (this is a first draft)
/cc:
The Logging Generator Author: @geeknoid
@davidfowl @stephentoub @shirhatti @333fred @ericstj
TODO (completed)
There is no constraint in the ordering at this point. So user is able to add an Exception, ILogger, LogLevel signature, or LogLevel, ILogger, Exception, or whatever. Sample: (note ILogger argument is the last argument)
Docs in the future would need to mention that event ID uniqueness is checked per class itself not at base classes.
Explain how the source generator detects the ILogger when using instance mode. Could we support logger APIs from base class or on interfaces?