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

OpenTelemetryLoggerProvider is now unaffected by changes to OpenTelemetryLoggerOptions after the LoggerFactory is built. #3055

Merged
merged 37 commits into from
Mar 30, 2022

Conversation

TimothyMothra
Copy link
Contributor

@TimothyMothra TimothyMothra commented Mar 15, 2022

Fixes #2902.

Changes

  • OpenTelemetryLoggerProvider will now copy the values from OpenTelemetryLoggerOptions. This prevents changes to the Provider after being initialized.
  • Updated tests. Some tests that were testing both true/false values needed to be broken up into two tests.
  • Added new tests to verify that changing Options after initializing the Provider don't affect the Provider.

Please provide a brief description of the changes here.

For significant contributions please make sure you have completed the following items:

  • Appropriate CHANGELOG.md updated for non-trivial changes
  • Design discussion issue #
  • Changes in public API reviewed

Options

I investigated 3 options.

  • Option 1,
    ctor of LoggerProvider copies Options to local fields.
    Logger.Log references this.provider.<local fields>.
    This introduces an ldfld.

  • Option 2,
    ctor of LoggerProvider copies Options to local fields.
    ctor of Logger copies Options to local fields.
    Logger.Log references this.<local fields>.

  • Option 3,
    ctor of LoggerProvider copies Options to local fields.
    Logger.Log first creates a local var for the provider var provider = this.provider then references all other properties/fields from the local var provider.<local fields>.
    This was done in an attempt to mitigate the ldfld.

Benchmarks

Test

For this test, I initialized the Provider and Logger.
For the benchmark, I invoke the hotpath (Logger.Log) which accesses the Options.

(ran benchmarks twice) 

|              Method |     Mean |    Error |   StdDev |  Gen 0 | Allocated |
|-------------------- |---------:|---------:|---------:|-------:|----------:|
|           Unchanged | 47.29 ns | 0.654 ns | 0.580 ns | 0.0102 |      64 B |
|       Option1_ldfld | 48.05 ns | 0.452 ns | 0.401 ns | 0.0102 |      64 B |
|   Option2_localCopy | 48.77 ns | 0.311 ns | 0.291 ns | 0.0102 |      64 B |
| Option3_varProvider | 46.99 ns | 0.411 ns | 0.385 ns | 0.0102 |      64 B |


|              Method |     Mean |    Error |   StdDev |  Gen 0 | Allocated |
|-------------------- |---------:|---------:|---------:|-------:|----------:|
|           Unchanged | 45.71 ns | 0.504 ns | 0.472 ns | 0.0102 |      64 B |
|       Option1_ldfld | 47.41 ns | 0.529 ns | 0.495 ns | 0.0102 |      64 B |
|   Option2_localCopy | 45.70 ns | 0.443 ns | 0.393 ns | 0.0102 |      64 B |
| Option3_varProvider | 46.18 ns | 0.567 ns | 0.531 ns | 0.0102 |      64 B |

The results are inconclusive, I'm seeing a negligible difference between implementations.

Benchmark code
  [MemoryDiagnoser]
  public class LogBenchmarks2
  {
      private readonly ILogger loggerUnchanged;
      private readonly ILogger loggerLdfld;
      private readonly ILogger loggerLocalCopy;
      private readonly ILogger loggerVarProvider;

      public LogBenchmarks2()
      {
          var options = new OpenTelemetryLoggerOptions
          {
              IncludeFormattedMessage = true,
              IncludeScopes = true,
              ParseStateValues = true,
          };

          var provider = new OpenTelemetryLoggerProvider(options);
          this.loggerUnchanged = new OpenTelemetryLogger("categoryName", provider);

          var provider2 = new OpenTelemetryLoggerProvider2(options);
          this.loggerLdfld = new OpenTelemetryLogger_LDFLD("categoryName", provider2);
          this.loggerLocalCopy = new OpenTelemetryLogger_LocalCopy("categoryName", provider2);
          this.loggerVarProvider = new OpenTelemetryLogger_VarProvider("categoryName", provider2);
      }

      [Benchmark]
      public void Unchanged()
      {
          this.loggerUnchanged.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
      }

      [Benchmark]
      public void Option1_ldfld()
      {
          this.loggerLdfld.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
      }

      [Benchmark]
      public void Option2_localCopy()
      {
          this.loggerLocalCopy.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
      }

      [Benchmark]
      public void Option3_varProvider()
      {
          this.loggerVarProvider.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
      }
  }

Option 1:

internal class OpenTelemetryLogger_LDFLD : ILogger

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
    if (!this.IsEnabled(logLevel)
        || Sdk.SuppressInstrumentation)
    {
        return;
    }

    var processor = this.provider.Processor;
    if (processor != null)
    {
        var record = new LogRecord(
            this.provider.IncludeScopes ? this.ScopeProvider : null,
            DateTime.UtcNow,
            this.categoryName,
            logLevel,
            eventId,
            this.provider.IncludeFormattedMessage ? formatter?.Invoke(state, exception) : null,
            this.provider.ParseStateValues ? null : state,
            exception,
            this.provider.ParseStateValues ? this.ParseState(state) : null);

        processor.OnEnd(record);

        record.ScopeProvider = null;
    }
}

Option 2:

internal class OpenTelemetryLogger_LocalCopy : ILogger

internal OpenTelemetryLogger_LocalCopy(string categoryName, OpenTelemetryLoggerProvider2 provider)
{
    Guard.ThrowIfNull(categoryName);
    Guard.ThrowIfNull(provider);

    this.categoryName = categoryName;
    this.provider = provider;

    this.IncludeScopes = provider.IncludeScopes;
    this.IncludeFormattedMessage = provider.IncludeFormattedMessage;
    this.ParseStateValues = provider.ParseStateValues;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
    if (!this.IsEnabled(logLevel)
        || Sdk.SuppressInstrumentation)
    {
        return;
    }

    var processor = this.provider.Processor;
    if (processor != null)
    {
        var record = new LogRecord(
            this.IncludeScopes ? this.ScopeProvider : null,
            DateTime.UtcNow,
            this.categoryName,
            logLevel,
            eventId,
            this.IncludeFormattedMessage ? formatter?.Invoke(state, exception) : null,
            this.ParseStateValues ? null : state,
            exception,
            this.ParseStateValues ? this.ParseState(state) : null);

        processor.OnEnd(record);

        record.ScopeProvider = null;
    }
}

Option 3

internal class OpenTelemetryLogger_VarProvider : ILogger

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
    if (!this.IsEnabled(logLevel)
        || Sdk.SuppressInstrumentation)
    {
        return;
    }

    var provider = this.provider;
    var processor = provider.Processor;
    if (processor != null)
    {
        var record = new LogRecord(
            provider.IncludeScopes ? this.ScopeProvider : null,
            DateTime.UtcNow,
            this.categoryName,
            logLevel,
            eventId,
            provider.IncludeFormattedMessage ? formatter?.Invoke(state, exception) : null,
            provider.ParseStateValues ? null : state,
            exception,
            provider.ParseStateValues ? this.ParseState(state) : null);

        processor.OnEnd(record);

        record.ScopeProvider = null;
    }
}

IL

The IL code is a bit more revealing.
Option 1 introduces additional ldfld when accessing every field.

Original, Option 2, and Option 3 are nearly identical.

ORIGINAL
	IL_002d: callvirt instance bool OpenTelemetry.Logs.OpenTelemetryLoggerOptions::get_IncludeScopes()

OPTION 1: LDFLD
	IL_0024: ldfld class OpenTelemetry.Logs.OpenTelemetryLoggerProvider2 OpenTelemetry.Logs.OpenTelemetryLogger_LDFLD::provider
	IL_0029: ldfld bool OpenTelemetry.Logs.OpenTelemetryLoggerProvider2::IncludeScopes

OPTION 2: LOCAL COPY
	IL_0021: ldfld bool OpenTelemetry.Logs.OpenTelemetryLogger_LocalCopy::IncludeScopes

OPTION 3: VAR PROVIDER
	IL_0023: ldfld bool OpenTelemetry.Logs.OpenTelemetryLoggerProvider2::IncludeScopes

Considering this, I'm recommending Option 3.

@TimothyMothra TimothyMothra requested a review from a team March 15, 2022 21:56
@codecov
Copy link

codecov bot commented Mar 15, 2022

Codecov Report

❗ No coverage uploaded for pull request base (main@add7eee). Click here to learn what that means.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #3055   +/-   ##
=======================================
  Coverage        ?   84.78%           
=======================================
  Files           ?      259           
  Lines           ?     9187           
  Branches        ?        0           
=======================================
  Hits            ?     7789           
  Misses          ?     1398           
  Partials        ?        0           
Impacted Files Coverage Δ
src/OpenTelemetry/Logs/OpenTelemetryLogger.cs 94.44% <100.00%> (ø)
.../OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs 82.35% <100.00%> (ø)

Copy link
Member

@cijothomas cijothomas left a comment

Choose a reason for hiding this comment

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

LGTM once a changelog entry is added

Co-authored-by: Cijo Thomas <cithomas@microsoft.com>
reyang
reyang previously requested changes Mar 18, 2022
Copy link
Member

@reyang reyang left a comment

Choose a reason for hiding this comment

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

Copy link
Member

@reyang reyang left a comment

Choose a reason for hiding this comment

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

LGTM.

@@ -30,38 +30,15 @@

namespace OpenTelemetry.Logs.Tests
{
public sealed class LogRecordTest : IDisposable
public sealed class LogRecordTest
Copy link
Member

Choose a reason for hiding this comment

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

The loggerfactory was disposed in IDisposable impln. It does not look like factory is disposed in the new tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed the helper method to return the factory. calling method is responsible for using using

Copy link
Member

@cijothomas cijothomas left a comment

Choose a reason for hiding this comment

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

Code changes looks good (Thanks for addressing the perf issues !)
The test changes does not seem to be disposing the factory. This needs to be fixed before we merge.

@@ -595,7 +595,7 @@ public void ParseStateValuesUsingCustomTest()
Assert.Same(state, actualState.Value);
}

private static void InitializeLoggerFactory(out ILogger logger, out List<LogRecord> exportedItems, Action<OpenTelemetryLoggerOptions> configure = null)
private static ILoggerFactory InitializeLoggerFactory(out ILogger logger, out List<LogRecord> exportedItems, Action<OpenTelemetryLoggerOptions> configure = null)
Copy link
Member

Choose a reason for hiding this comment

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

why do we have to have the helper method create a ILogger? It can just do what it claims to do (from the name) - initialize and return a loggerfactory, configured as per the configure action given to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was meant to simplify the individual tests by removing a dozen lines of identical boilerplate code.
I can move some or all of this into every test, but I don't think that improves the value of the tests themselves.

Copy link
Member

Choose a reason for hiding this comment

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

The name of the helper method did not indicate its intended purpose. The latest commit looks better.
(if we want to achieve less duplication in tests, we can do that, with aptly named helper. but this looks good for me now)

@TimothyMothra TimothyMothra changed the title prevent OpenTelemetryLoggerOptions from being modified after init. OpenTelemetryLoggerProvider is now unaffected by changes to OpenTelemetryLoggerOptions after the LoggerFactory is built. Mar 29, 2022
@cijothomas cijothomas merged commit eefffa9 into open-telemetry:main Mar 30, 2022
@TimothyMothra TimothyMothra deleted the 2902_fix_options branch March 30, 2022 16:17
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.

Disallow LogRecord options to be updated after initialization.
3 participants