-
Notifications
You must be signed in to change notification settings - Fork 772
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
Logs: Buffer log state values to fix disposing HttpContext #2932
Logs: Buffer log state values to fix disposing HttpContext #2932
Conversation
Codecov Report
@@ Coverage Diff @@
## main #2932 +/- ##
==========================================
+ Coverage 83.86% 83.96% +0.10%
==========================================
Files 254 254
Lines 8941 8966 +25
==========================================
+ Hits 7498 7528 +30
+ Misses 1443 1438 -5
|
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.
just a few minor (nit) comments
src/OpenTelemetry/Logs/LogRecord.cs
Outdated
// See: | ||
// https://github.com/open-telemetry/opentelemetry-dotnet/issues/2905 | ||
|
||
this.bufferedStateValues = new(this.stateValues); |
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.
does this copy the whole collection to bufferedStateValues? if not, we'll still have the same issue, when we eventually iteration on bufferedStateValues?
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.
It definitely works because I started with a failing test and then I made it pass. BUT... there are two branches in List<T>
I was expecting us to hit the first one which I thought would be faster. Looking at it right now, we are probably falling into the second one which just does an enumeration. Because IReadOnlyList<T>
does not implement ICollection<T>
. Oh .NET collections! That enumeration is probably costing us some allocation we can avoid. Let me perf test it a bit and I'll reply back.
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.
@cijothomas OK I did some perf testing. Perf depends on the concrete type of the state. When there is an ICollection<T>
the CopyTo
directly over the array gives a nice boost. When there is only IReadOnlyList
we can avoid an allocation using a for
loop instead of using the enumerator. For other states, we already make a copy so there isn't any need to buffer at all. I added some type checking to optimize for those cases.
Let me know what you think about that.
Method | NumberOfItemsInState | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
---|---|---|---|---|---|---|---|
BufferListState | 0 | 38.79 ns | 0.480 ns | 0.401 ns | 0.0108 | - | 136 B |
BufferReadOnlyState | 0 | 37.15 ns | 0.324 ns | 0.253 ns | 0.0108 | - | 136 B |
BufferCopiedState | 0 | 39.59 ns | 0.565 ns | 0.472 ns | 0.0108 | - | 136 B |
BufferListState | 100 | 142.18 ns | 1.753 ns | 1.369 ns | 0.1402 | 0.0007 | 1,760 B |
BufferReadOnlyState | 100 | 648.79 ns | 11.549 ns | 10.803 ns | 0.1402 | - | 1,760 B |
BufferCopiedState | 100 | 39.69 ns | 0.807 ns | 0.829 ns | 0.0108 | - | 136 B |
{ | ||
if (this.ScopeProvider == null || this.bufferedScopes != null) | ||
if (this.stateValues != null && this.bufferedStateValues == null) |
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.
https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs#L67 should we set to null similar to being done for scopes?
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 like it! Updated so state works the same. Had to update a lot of tests but I think it was worth it. LMK what you think.
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.
+1
src/OpenTelemetry/Logs/LogRecord.cs
Outdated
/// <remarks> | ||
/// Note: StateValues are only available during the lifecycle of the log | ||
/// message being written. If you need to capture state to be used later | ||
/// (for example in batching scenarios), call <see cref="Buffer"/> to |
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.
Buffer
is an internal API and StateValues
is public. Might lead to confusion being in the comment here. Should Buffer
be a public API since maybe there are other use cases besides the batching processor where someone extending the SDK would want access to call Buffer
?
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.
@alanwest I think it was public originally but then in code review on the initial PR we made it internal. Didn't catch the comments, sorry.
I took this in a different direction. Check out this commit: c057ea0
The change yesterday to clear state after processing LogRecord
s introduced/exposed an issue in our InMemoryExporter
. Basically, it isn't very useful because the exported LogRecords don't have states or scopes available!
I added a setting on SimpleLogRecordExportProcessor
which the InMemoryExporter
log extension now uses to turn on buffering. It is currently public. If we leave it public, I think we can keep LogRecord.Buffer
internal. Basically now if users need buffering they can either use SimpleLogRecordExportProcessor
with the new property or BatchLogRecordExportProcessor
which always buffers. I updated the comments referenced here to point to the public stuff.
/cc @cijothomas @reyang
return loggerOptions.AddProcessor( | ||
new SimpleLogRecordExportProcessor(new InMemoryExporter<LogRecord>(exportedItems)) | ||
{ | ||
BufferLogData = true, |
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.
Is this for InMemoryExporter scenario, or there are more use cases?
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.
Check out the comment above this one. I was typing that out while you were reviewing 😄 I think it has general use, which is why I made it public. But it doesn't have to be, we could make it internal and use InternalsVisibleTo
to allow InMemoryExporter
project to set it. Or figure out something else entirely!
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'm interested about the general use.
I thought InMemoryExporter is the only odd ball here because it allows users to examine the log records after Export()
has returned (and I was thinking maybe we just change AddInMemoryExporter to use batch processor 😆). Do you see other cases where log records are consumed outside Export()
?
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.
@reyang I couldn't up with any good concrete use cases for it to be public so I redid the change. Take a look and let me know what you think. I gave the log in-memory registration extension its own processor to control the buffering. No impact to actual workloads that way. Used InternalsVisibleTo in order to allow that processor to call Buffer
without a public API change. I did try using the batch processor, but that changed the semantics. Tests need to spin or call force flush I think keeping it on simple keeps the tests simple 😉
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.
Take a look and let me know what you think. I gave the log in-memory registration extension its own processor to control the buffering. Used InternalsVisibleTo in order to allow that processor to call Buffer without a public API change.
+1 Looks solid 👍 I think InMemoryExporter
would justify InternalsVisibleTo
because the fact that it allows data to be examined after Export()
returned is making it special comparing to all other exporters.
I did try using the batch processor, but that changed the semantics. Tests need to spin or call force flush I think keeping it on simple keeps the tests simple.
These are the pros and cons I could think of:
- [minus] The Batch version will require ForceFlush, which is a semantic change (we can imagine folks who already rely on the Simple behavior would see test breaks).
- [minus] the Batch version might drop the data on the floor if the circular buffer runs out of space, making it a bit unpredictable.
- [plus] The Batch version makes InMemoryExporter suitable for perf test, because it doesn't have a global lock like the Simple version (so callers won't yield control when there is contention).
- [plus] The Batch version makes the semantic consistent across logs/metrics/traces - because in Metrics case folks have to ForceFlush anyways.
I would weight 1 > 2 > 3 > 4. The change you've made seems to be the best balance IMHO.
Will need to review in deep, so holding off from merging. The immediate crash issue is addressed separately As discussed in today's SIG, we might want to explore and see if it is feasible to optimize for the common patter (i.e using extension methods like LogInformation, source-generation.etc), which provides state as IReadOnlyList<string,object>, and not ICollection. |
This PR was marked stale due to lack of activity and will be closed in 7 days. Commenting or Pushing will instruct the bot to automatically remove the label. This bot runs once per day. |
Closed as inactive. Feel free to reopen if this PR is still being worked on. |
Fixes #2905
Changes
BatchLogRecordExportProcessor
will now buffer log StateValues to fix the issue caused by ASP.NET Core hosting state wrapping HttpContext which can be disposed before export has triggered.TODOs
CHANGELOG.md
updated for non-trivial changes