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] Add OpenTelemetrySdk builder pattern #5325

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

CodeBlanch
Copy link
Member

@CodeBlanch CodeBlanch commented Feb 7, 2024

Relates to #5137
Relates to #5265

Changes

  • Adds OpenTelemetrySdk API.

Details

#5265 laid the groundwork for cross-cutting extensions (ex #5400). The issue is (currently) only users of OpenTelemetry.Extensions.Hosting will be able to utilize those. What this PR does is add a pattern in SDK for non-hosting users (primarily .NET Framework) to utilize all the cross-cutting things. It also has the benefit of potentially unifying our startup/configuration story/documentation so everything uses the same "With" style (see #5262 (comment)).

We have already seen distros working around this gap by rolling their own custom builders for example: https://github.com/elastic/elastic-otel-dotnet/blob/main/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs

Usage

New style:

        using var openTelemetry = OpenTelemetrySdk.Create(builder => builder
            .ConfigureResource(r => r.AddService("MyService"))
            .UseOtlpExporter());

        // To access the providers:
        // openTelemetry.LoggerProvider (Experimental API)
        // openTelemetry.MeterProvider
        // openTelemetry.TracerProvider

        // To get the ILoggerFactory:
        // openTelemetry.GetLoggerFactory();

Old style:

        var resourceBuilder = ResourceBuilder.CreateDefault().AddService("MyService");

        using var tracerProvider = Sdk.CreateTracerProviderBuilder()
            .SetResourceBuilder(resourceBuilder)
            .AddOtlpExporter()
            .Build();

        using var meterProvider = Sdk.CreateMeterProviderBuilder()
            .SetResourceBuilder(resourceBuilder)
            .AddOtlpExporter()
            .Build();

        // Experimental API
        using var loggerProvider = Sdk.CreateLoggerProviderBuilder()
            .SetResourceBuilder(resourceBuilder)
            .AddOtlpExporter()
            .Build();

        using var loggerFactory = LoggerFactory.Create(logging =>
        {
            logging.AddOpenTelemetry(builder => builder
                .SetResourceBuilder(resourceBuilder)
                .AddOtlpExporter());
        });

Public API changes

namespace OpenTelemetry;

public sealed class OpenTelemetrySdk : IDisposable
{
   public IServiceProvider Services { get; }
   public MeterProvider MeterProvider { get; }
   public TracerProvider TracerProvider { get; }
#if EXPOSE_EXPERIMENTAL_FEATURES
   public LoggerProvider LoggerProvider { get; }
#endif
   public void Dispose() {}
}

public static class OpenTelemetrySdkExtensions
{
   public static ILoggerFactory GetLoggerFactory(this OpenTelemetrySdk sdk) {}
}

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

@CodeBlanch CodeBlanch added the pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package label Feb 7, 2024
@rajkumar-rangaraj
Copy link
Contributor

Could you explain how this will be handled

using var openTelemetry = OpenTelemetrySdk.Create(builder => builder
            .ConfigureResource(r => r.AddService("MyService"))
            .AddOtlpExporter() // (Future thing)
            .WithTracing(builder => builder.AddOtlpExporter(otlpOptions  => {}));

@CodeBlanch
Copy link
Member Author

Could you explain how this will be handled

using var openTelemetry = OpenTelemetrySdk.Create(builder => builder
            .ConfigureResource(r => r.AddService("MyService"))
            .AddOtlpExporter() // (Future thing)
            .WithTracing(builder => builder.AddOtlpExporter(otlpOptions  => {}));

Are you asking about the options? Or just two calls to "AddOtlpExporter" in there? I'm going to assume the later.

@alanwest Raised the same question here: #5137 (comment)

This isn't the PR for that discussion, really. We'll work through that when we add the AddOtlpExporter cross-cutting extension.

But my current thinking is: It is a non-issue 🤣

It will be handled IMO the same way this is handled...

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddOtlpExporter()
    .AddOtlpExporter()
    .Build();

You will get two exporters. There are perfectly valid reasons to do that so I don't think we should assume anything or guess what the user wants. But we can work through it and discuss it more when we get there.

@CodeBlanch CodeBlanch added metrics logs Logging signal related traces Tracing signal related labels Feb 12, 2024
@CodeBlanch CodeBlanch marked this pull request as ready for review February 12, 2024 19:06
@CodeBlanch CodeBlanch requested a review from a team as a code owner February 12, 2024 19:06
@reyang
Copy link
Member

reyang commented Feb 12, 2024

Could you explain how this will be handled

using var openTelemetry = OpenTelemetrySdk.Create(builder => builder
            .ConfigureResource(r => r.AddService("MyService"))
            .AddOtlpExporter() // (Future thing)
            .WithTracing(builder => builder.AddOtlpExporter(otlpOptions  => {}));

@CodeBlanch I have a general question regarding the design, could you summarize 1) what you would like to achieve and 2) the design principles?
E.g. each signal allows the user to add plugins, how these plugins work with "global" plugins? (basically, what rules do we follow)

Take one concrete example (note that this is just to facilitate the discussion, we won't be able to enumerate all scenarios, a set of rules are needed here):

I want to have OTLP exporter for all 3 signals, however, I want to set the metrics exporting frequency to 10 seconds, and logs/traces to 1 minute. I also want to put my custom redaction processor before the OTLP log exporter.

@CodeBlanch
Copy link
Member Author

@CodeBlanch I have a general question regarding the design, could you summarize 1) what you would like to achieve and 2) the design principles?

What I want to achieve is primarily the simplification of the startup and teardown code. One call to make which initializes the SDK and one call to make to dispose/shutdown the SDK. The design principle is "crawl, walk, run" is there a name for that? 🤣 The secondary goal is to simplify docs and examples using this.

Take one concrete example (note that this is just to facilitate the discussion, we won't be able to enumerate all scenarios, a set of rules are needed here):

I want to have OTLP exporter for all 3 signals, however, I want to set the metrics exporting frequency to 10 seconds, and logs/traces to 1 minute. I also want to put my custom redaction processor before the OTLP log exporter.

This PR is NOT adding a cross-cutting AddOtlpExporter method 🚫 I really don't want to wade into that. We'll have to solution that out and it may never manifest. What this PR is doing is allowing users to do this:

// New
using var sdk = OpenTelemetrySdk.Create(builder => builder
    .ConfigureResource(r => r.AddService("MyApp"))
    .WithLogging(builder => builder.AddOtlpExporter())
    .WithMetrics(builder => builder.AddOtlpExporter())
    .WithTracing(builder => builder.AddOtlpExporter()));

// Old
var resource = ResourceBuilder.CreateDefault()
    .AddService("MyApp");

using var loggingProvider = Sdk.CreateLoggerProviderBuilder()
    .SetResourceBuilder(resource)
    .AddOtlpExporter()
    .Build();

using var meterProdier = Sdk.CreateMeterProviderBuilder()
    .SetResourceBuilder(resource)
    .AddOtlpExporter()
    .Build();

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .SetResourceBuilder(resource)
    .AddOtlpExporter()
    .Build();

It is a convenience thing allowing the "manual" bootstrap pattern to utilize the "With" pattern we have in hosting. The main benefit is it allows you to track a single thing to dispose. Seems minor, but most code I see does NOT clean up things correctly 😭

Scenarios

Since you brought up scenarios let's imagine where we could take this.

Crawl: I want to have OTLP exporter for all 3 signals

[My guess is this scenario is what most users want to do.]

With this PR we don't have a cross-cutting AddOtlpExporter so you do this:

using var sdk = OpenTelemetrySdk.Create(builder => builder
    .ConfigureResource(r => r.AddService("MyApp"))
    .WithLogging(builder => builder.AddOtlpExporter())
    .WithMetrics(builder => builder.AddOtlpExporter())
    .WithTracing(builder => builder.AddOtlpExporter()));

How would it work once we have a cross-cutting method?

using var sdk = OpenTelemetrySdk.Create(builder => builder
   .ConfigureResource(r => r.AddService("MyApp"))
   .AddOtlpExporter()
   .WithLogging()
   .WithMetrics()
   .WithTracing());

Walk: I want to have OTLP exporter for all 3 signals sending to different endpoints

With this PR we don't have a cross-cutting AddOtlpExporter so you do this:

using var sdk = OpenTelemetrySdk.Create(builder => builder
    .ConfigureResource(r => r.AddService("MyApp"))
    .WithLogging(builder => builder.AddOtlpExporter(o => o.Endpoint = new("http://mylogendpoint/logs")))
    .WithMetrics(builder => builder.AddOtlpExporter(o => o.Endpoint = new("http://mymetricendpoint/metrics")))
    .WithTracing(builder => builder.AddOtlpExporter(o => o.Endpoint = new("http://mytraceendpoint/traces"))));

How would it work once we have a cross-cutting method? Any number of ways. It depends on what options we expose, the API shape, etc. It could look like this:

using var sdk = OpenTelemetrySdk.Create(builder => builder
    .ConfigureResource(r => r.AddService("MyApp"))
    .AddOtlpExporter(options => {
         options.LoggingOptions.Endpoint = new("http://mylogendpoint/logs");
         options.MetricsOptions.Endpoint = new("http://mymetricendpoint/metrics");
         options.TracingOptions.Endpoint = new("http://mytraceendpoint/trace");
    })
    .WithLogging()
    .WithMetrics()
    .WithTracing());

Run: I want to have OTLP exporter for all 3 signals, however, I want to set the metrics exporting frequency to 10 seconds, and logs/traces to 1 minute. I also want to put my custom redaction processor before the OTLP log exporter.

Different ways you could achieve this. If we had a simple AddOtlpExporter cross-cutting extension, you could do this:

using var sdk = OpenTelemetrySdk.Create(builder =>
{
    builder.Services.Configure<MetricReaderOptions>(o => o.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10_000);
    builder.Services.Configure<BatchExportActivityProcessorOptions>(o => o.ScheduledDelayMilliseconds = 60_000);
    builder.Services.Configure<LogRecordExportProcessorOptions>(o => o.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 60_000);

    builder
        .ConfigureResource(r => r.AddService("MyApp"))
        .WithLogging(logging => logging.AddProcessor(new CustomLogRedactionProcessor()))
        .WithMetrics()
        .WithTracing()
        .AddOtlpExporter();
});

Registration order

E.g. each signal allows the user to add plugins, how these plugins work with "global" plugins? (basically, what rules do we follow)

Essentially it works the same way we have it today when using the hosting package. Nothing here is really new. The code I have above:

    builder
        .ConfigureResource(r => r.AddService("MyApp"))
        .WithLogging(logging => logging.AddProcessor(new CustomLogRedactionProcessor()))
        .WithMetrics()
        .WithTracing()
        .AddOtlpExporter();

Is just orchestrating these calls:

    services.ConfigureOpenTelemetryLoggerProvider(builder => builder.ConfigureResource(r => r.AddService("MyApp")));
    services.ConfigureOpenTelemetryMeterProvider(builder => builder.ConfigureResource(r => r.AddService("MyApp")));
    services.ConfigureOpenTelemetryTracerProvider(builder => builder.ConfigureResource(r => r.AddService("MyApp")));
    services.ConfigureOpenTelemetryLoggerProvider(builder => builder.AddProcessor(new CustomLogRedactionProcessor()));
    services.ConfigureOpenTelemetryLoggerProvider(builder => builder.AddOtlpExporter());
    services.ConfigureOpenTelemetryMeterProvider(builder => builder.AddOtlpExporter());
    services.ConfigureOpenTelemetryTracerProvider(builder => builder.AddOtlpExporter());

The order in which they are called is the order in which they are applied.

Same ordering requirements are actually in play for the APIs we have today...

This user is happy:

    using var loggerFactory = LoggerFactory.Create(builder => builder
        .AddOpenTelemetry(options => options
            .AddProcessor(new CustomLogRedactionProcessor()))
            .AddOtlpExporter());

This user is sad:

    using var loggerFactory = LoggerFactory.Create(builder => builder
        .AddOpenTelemetry(options => options
            .AddOtlpExporter()
            .AddProcessor(new CustomLogRedactionProcessor())));

@reyang
Copy link
Member

reyang commented Feb 13, 2024

This PR is NOT adding a cross-cutting AddOtlpExporter method 🚫 I really don't want to wade into that. We'll have to solution that out and it may never manifest.

This is exactly why I think it is unclear, the PR description mentioned:

        using var openTelemetry = OpenTelemetrySdk.Create(builder => builder
            .ConfigureResource(r => r.AddService("MyService"))
            .AddOtlpExporter() // (Future thing)
            .AddConsoleExporter() // (Future thing)
            .WithTracing()
            .WithLogging() // (Experimental API)
            .WithMetrics());

        // To access the providers:
        // openTelemetry.LoggerProvider (Experimental API)
        // openTelemetry.MeterProvider
        // openTelemetry.TracerProvider

        // To get the ILoggerFactory:
        // openTelemetry.GetLoggerFactory();

What I want - the overall goal/direction, I cannot review a concrete PR without first understanding what's the overall goal, if the PR is adding a new way of doing things. Specifically, I'm concerned that we end up with a set of incomplete stories "as a user, you can choose path A, B or C, and all of them come with caveats that you should know ahead of time".

@alanwest
Copy link
Member

alanwest commented Feb 16, 2024

This PR is NOT adding a cross-cutting AddOtlpExporter method 🚫 I really don't want to wade into that. We'll have to solution that out and it may never manifest.

This brings me some pause as well. My original impression was that manifesting this was the north star for all this work. If it remains uncertain whether this goal will be achievable, then I agree more thought and design is required before we commit to landing anything.

This may entail reevaluating the work already landed in #5265 - at a bare minimum, it sounds like the new APIs introduced in #5265 should be marked experimental, so that in the event we don't end up shipping any cross-cutting extensions we can delete the experimental APIs.

I think the AddOtlpExporter method needs to remain the steel thread for vetting this work. I agree that the work in this PR may be nice in the end, but I don't think it contributes directly to the steel thread, so it is of lower priority at the moment.

A number of open questions were raised by folks in a recent SIG meeting which I captured in this comment
#5137 (comment). I think circling back to these questions and vetting out possible answers to them should be the primary focus.

@CodeBlanch
Copy link
Member Author

Let's merge this and I can do a follow-up which makes it all experimental?

@reyang
Copy link
Member

reyang commented Feb 16, 2024

Let's merge this and I can do a follow-up which makes it all experimental?

I disagree.

Copy link
Contributor

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.

@github-actions github-actions bot added the Stale label Feb 24, 2024
@CodeBlanch CodeBlanch marked this pull request as draft February 28, 2024 00:37
@github-actions github-actions bot removed the Stale label Feb 28, 2024
Copy link
Contributor

github-actions bot commented Mar 6, 2024

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.

@github-actions github-actions bot added the Stale label Mar 6, 2024
Copy link
Contributor

Closed as inactive. Feel free to reopen if this PR is still being worked on.

@github-actions github-actions bot closed this Mar 13, 2024
@CodeBlanch CodeBlanch reopened this Mar 13, 2024
@CodeBlanch
Copy link
Member Author

@reyang @alanwest The usage on the PR description now shows how this API works with UseOtlpExporter (#5400).

@CodeBlanch CodeBlanch marked this pull request as ready for review March 13, 2024 19:40
Copy link

codecov bot commented Mar 13, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 85.71%. Comparing base (6250307) to head (b5872d8).
Report is 228 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #5325      +/-   ##
==========================================
+ Coverage   83.38%   85.71%   +2.33%     
==========================================
  Files         297      273      -24     
  Lines       12531    11407    -1124     
==========================================
- Hits        10449     9778     -671     
+ Misses       2082     1629     -453     
Flag Coverage Δ
unittests ?
unittests-Solution-Experimental 85.66% <100.00%> (?)
unittests-Solution-Stable 85.56% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
src/OpenTelemetry/OpenTelemetrySdk.cs 100.00% <100.00%> (ø)
src/OpenTelemetry/OpenTelemetrySdkExtensions.cs 100.00% <100.00%> (ø)

... and 100 files with indirect coverage changes

@github-actions github-actions bot removed the Stale label Mar 14, 2024
Copy link
Contributor

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.

@github-actions github-actions bot added Stale and removed Stale labels Mar 22, 2024
@rajkumar-rangaraj
Copy link
Contributor

A few customers might do something like the following and expect ASP.NET Core telemetries to be collected. However, since these APIs create their own tracer provider, customers may not see the expected data:

        using var openTelemetry = OpenTelemetrySdk.Create(builder => builder
            .ConfigureResource(r => r.AddService("MyService"))
            .UseOtlpExporter());

        using var tracerProvider = Sdk.CreateTracerProviderBuilder()
            .AddAspNetCoreInstrumentation()
            .Build();

If we proceed with this approach, the simultaneous use of OpenTelemetrySdk.Create() and Sdk.CreateTracerProviderBuilder() should not be allowed. If both methods are used, one should throw an exception to clearly state that these APIs cannot be combined.

Copy link
Contributor

github-actions bot commented Apr 3, 2024

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.

@github-actions github-actions bot added Stale and removed Stale labels Apr 3, 2024
Copy link
Contributor

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.

@github-actions github-actions bot added Stale and removed Stale labels Apr 12, 2024
Copy link
Contributor

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.

@github-actions github-actions bot added the Stale label Apr 20, 2024
@matt-hensley
Copy link
Contributor

This would be a nice addition for distribution authors and simplify documenting proper SDK setup, especially for .NET Framework applications.

@github-actions github-actions bot removed the Stale label Apr 27, 2024
Copy link
Member

@alanwest alanwest left a comment

Choose a reason for hiding this comment

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

I'm supportive of bringing the new IOpenTelemetryBuilder to .NET Framework and console application use cases. The ability to ship cross-cutting extension methods for all .NET use cases is valuable.

Left a couple questions.

Also, please respond to @rajkumar-rangaraj's question #5325 (comment). I know we discussed it in a recent SIG meeting, but it would help to refresh where we landed.

/// <summary>
/// Gets the <see cref="IServiceProvider"/> containing SDK services.
/// </summary>
public IServiceProvider Services => this.serviceProvider;
Copy link
Member

Choose a reason for hiding this comment

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

What kind of use case is served by exposing the service provider? What if we didn't?

/// </remarks>
/// <param name="sdk"><see cref="OpenTelemetrySdk"/>.</param>
/// <returns><see cref="ILoggerFactory"/>.</returns>
public static ILoggerFactory GetLoggerFactory(this OpenTelemetrySdk sdk)
Copy link
Member

Choose a reason for hiding this comment

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

Exploring the .NET Framework and console app use cases for a sec...

If a user has already adopted ILogger as their logging framework, then they probably have code somewhere like

var loggerFactory = LoggerFactory.Create((loggingBuilder) => {
    loggingBuilder.AddConsole() ...
});

If they then use WithLogging, they'll need to refactor things to avoid having to interact with two LoggerFactory instances to get loggers.

Are there other options that might minimize the need for refactoring? Would an additional overload of WithLogging accepting a LoggerFactory as an argument help? My assumption is that if they pass in their existing LoggerFactory then we'd be able to add the OpenTelemetry logger provider.

Copy link
Contributor

github-actions bot commented May 9, 2024

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.

@github-actions github-actions bot added Stale and removed Stale labels May 9, 2024
Copy link
Contributor

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.

@github-actions github-actions bot added Stale and removed Stale labels May 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
logs Logging signal related metrics pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package traces Tracing signal related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants