Skip to content
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

[sdk-1.5.0-hotfix] Fix LogRecord.State being null when TState matches known interfaces #4609

Merged

Conversation

CodeBlanch
Copy link
Member

@CodeBlanch CodeBlanch commented Jun 23, 2023

Note: This PR is targeting main-1.5.0 branch.

Changes

  • Restores 1.4.0 logic for LogRecord.State handling.

Details

1.5.0 tweaked OpenTelemetryLoggerOptions.ParseStateValues so that if logged TState is IReadOnlyList or IEnumerable of KeyValuePair<string, object> we automatically set LogRecord.Attributes \ LogRecord.StateValues and leave LogRecord.State = null. The idea with that is exporters were coded like this...

if (logRecord.StateValues != null)
{
    attributes = logRecord.StateValues;
}
else
{
    attributes = logRecord.State as IReadOnlyList<KeyValuePair<string, object>>;
}

What was reported is an exporter doing this...

var attributes = (IReadOnlyCollection<KeyValuePair<string, object>>)logRecord.State;

Which throws a NRE with the new logic.

In order to preserve the above logic this PR puts back handling of LogRecord.State so that it is always set so long as OpenTelemetryLoggerOptions.ParseStateValues == false (the previous behavior).

Merge requirement checklist

  • CONTRIBUTING guidelines followed (nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes

@CodeBlanch CodeBlanch requested a review from a team June 23, 2023 19:06
@codecov
Copy link

codecov bot commented Jun 23, 2023

Codecov Report

Merging #4609 (3a46aeb) into main-1.5.0 (494323b) will increase coverage by 0.00%.
The diff coverage is 100.00%.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           main-1.5.0    #4609   +/-   ##
===========================================
  Coverage       85.57%   85.57%           
===========================================
  Files             320      320           
  Lines           12615    12620    +5     
===========================================
+ Hits            10795    10800    +5     
  Misses           1820     1820           
Impacted Files Coverage Δ
.../OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs 88.28% <100.00%> (+0.55%) ⬆️

... and 6 files with indirect coverage changes

bool parseStateValues)
{
iLoggerData.State = null;
if (!includeAttributes)
Copy link
Contributor

@utpilla utpilla Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add check for state == null here and remove the else if condition here?

Suggested change
if (!includeAttributes)
if (!includeAttributes || state is null)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the else if could only be removed if we also take your suggestion:

How about we also remove the support for parsing custom typed objects as well in 1.5.1? This PR #4560 is doing that anyway?

Right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking here was most states will not be null so better to not spend cycles checking for uncommon case until it is needed. But I moved it up and rewrote the check so that the JIT should elide it for structs completely so there shouldn't be any impact.

@utpilla
Copy link
Contributor

utpilla commented Jun 23, 2023

How about we also remove the support for parsing custom typed objects as well in 1.5.1? This PR #4560 is doing that anyway?

@CodeBlanch
Copy link
Member Author

How about we also remove the support for parsing custom typed objects as well in 1.5.1? This PR #4560 is doing that anyway?

I think for a hotfix we should keep it small and focused. But if you want to also do that in there we can do on a follow-up.

@alanwest
Copy link
Member

How about we also remove the support for parsing custom typed objects as well in 1.5.1? This PR #4560 is doing that anyway?

I think for a hotfix we should keep it small and focused. But if you want to also do that in there we can do on a follow-up.

Also agree should be a separate PR. Though, is there a strong reason #4560 should ship in 1.5.1?

@utpilla
Copy link
Contributor

utpilla commented Jun 23, 2023

Also agree should be a separate PR.

I agree to that as well.

Though, is there a strong reason #4560 should ship in 1.5.1?

I think it's better to treat that as an unwanted change (bug?) added in 1.5.0 and remove it for 1.5.1 instead of making a breaking change in 1.6.0.

@alanwest
Copy link
Member

I think it's better to treat that as an unwanted change (bug?)

👍 I support this reasoning.

@reyang
Copy link
Member

reyang commented Jun 23, 2023

I think it's better to treat that as an unwanted change (bug?) added in 1.5.0 and remove it for 1.5.1 instead of making a breaking change in 1.6.0.

+1 Good judgement!

@CodeBlanch
Copy link
Member Author

I just pushed a change...

  • Removed the date from the CHANGELOG since it seems like we will be doing more for the hotfix.
  • Updated the logic after doing some perf testing:

It seems the compiler is not smart enough to know when it has boxed some struct. The code was flowing like this:

logRecord.State = state; // boxing 1
if (state is IReadOnlyList<KeyValuePair<string, object?>> stateList)) // boxing 2

This PR makes the memory perf twice as bad and the CPU has to do more work 🤮

I changed it to do this...

if (state is IReadOnlyList<KeyValuePair<string, object?>> stateList))
{
    logRecord.State = stateList; // reuse boxing of state to IReadOnlyList
}
else
{
    logRecord.State = state; // box directly if all else failed
}

It is more code but the perf justifies it IMO.

Benchmarks
[MemoryDiagnoser]
public class BoxingBenchmarks
{
    [Benchmark]
    public (object State, int Count) AssignToObjectFirstAndThenCastAsInterface()
    {
        var data = default(TestStruct);

        var state = (object)data;

        if (data is IReadOnlyList<KeyValuePair<string, object>> list)
        {
            return (state, list.Count);
        }

        return (state, -1);
    }

    [Benchmark]
    public (object State, int Count) CastAsInterfaceAndThenAssignAsState()
    {
        var data = default(TestStruct);

        if (data is IReadOnlyList<KeyValuePair<string, object>> list)
        {
            return (list, list.Count);
        }

        return ((object)data, -1);
    }

    public struct TestStruct : IReadOnlyList<KeyValuePair<string, object>>
    {
        public int IntValue;
        public double DoubleValue;
        public object RefValue;

        public readonly int Count => 0;

        public readonly KeyValuePair<string, object> this[int index] => throw new NotImplementedException();

        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            throw new NotImplementedException();
        }
    }
}
Method Mean Error StdDev Gen0 Gen1 Allocated
AssignToObjectFirstAndThenCastAsInterface 7.717 ns 0.1748 ns 0.1460 ns 0.0064 0.0000 80 B
CastAsInterfaceAndThenAssignAsState 4.945 ns 0.1083 ns 0.1013 ns 0.0032 0.0000 40 B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants