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

Add OpenTelemetry.Hosting #253

Closed
wants to merge 4 commits into from

Conversation

JamesNK
Copy link
Contributor

@JamesNK JamesNK commented Oct 2, 2019

Fixes #241

This PR adds a OpenTelemetry.Hosting package for improving integration between OpenTelemetry and DI (could also be named OpenTelemetry.DependencyInjection)

I've kept the DI dependency isolated to this package. The user experience could be improved with strongly typed registration methods, e.g. AddZipkin(options), but that would require either additional projects depending on DI, or more packages for DI integration, e.g. OpenTelemetry.Exporter.Zipkin.DependencyInjection.

To start collectors I have an IHostedService registered for each. It will automatically start and stop collectors, avoiding the current hack of users manually resolving services somewhere to ensure they are created. Generic host experts: It feels like I'm abusing what an IHostedService is for to inject start and stop logic via DI. Let me know if there is a better way of doing this. @davidfowl @glennc @pakrym

After:

services.AddOpenTelemetry(telemetry =>
{
    telemetry.AddCollector<RequestsCollector>(new RequestsCollectorOptions());
    telemetry.AddCollector<DependenciesCollector>(new DependenciesCollectorOptions());
    telemetry.SetTracer<Tracer, TraceConfig>();
    telemetry.SetSpanExporter<ZipkinTraceExporter>(new ZipkinTraceExporterOptions { ServiceName = "my-service" });
});

Before:

services.AddSingleton<ISampler>(Samplers.AlwaysSample);
services.AddSingleton<ZipkinTraceExporterOptions>(_ => new ZipkinTraceExporterOptions { ServiceName = "my-service" });
services.AddSingleton<SpanExporter, ZipkinTraceExporter>();
services.AddSingleton<SpanProcessor, BatchingSpanProcessor>();
services.AddSingleton<TraceConfig>();
services.AddSingleton<ITracer, Tracer>();

// you may also configure request and dependencies collectors
services.AddSingleton<RequestsCollectorOptions>(new RequestsCollectorOptions());
services.AddSingleton<RequestsCollector>();

services.AddSingleton<DependenciesCollectorOptions>(new DependenciesCollectorOptions());
services.AddSingleton<DependenciesCollector>();
public void Configure(IApplicationBuilder app, RequestsCollector requestsCollector,  DependenciesCollector dependenciesCollector)
{
    // ...
}

@@ -52,7 +52,7 @@ private TracerShim(Trace.ITracer tracer)
throw new ArgumentNullException(nameof(format));
}

if (carrier == default)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

VS complained about this 🤷‍♂

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a bug (only in internal builds) being fixed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I ended up changing those lines to carrier == null (for now).


public Task StopAsync(CancellationToken cancellationToken)
{
(this.collector as IDisposable)?.Dispose();
Copy link
Contributor

Choose a reason for hiding this comment

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

Do this in dispose (implement IDisposable)

@davidfowl
Copy link
Contributor

This still looks noisy:

services.AddOpenTelemetry(telemetry =>
{
    telemetry.AddCollector<RequestsCollector>(new RequestsCollectorOptions());
    telemetry.AddCollector<DependenciesCollector>(new DependenciesCollectorOptions());
    telemetry.SetTracer<Tracer, TraceConfig>();
    telemetry.SetSpanExporter<ZipkinTraceExporter>(new ZipkinTraceExporterOptions { ServiceName = "my-service" });
});

Is there anyway to get it down to something like:

services.AddOpenTelemetry(telemetry =>
{
    telemetry.AddCollector<RequestsCollector>();
    telemetry.AddCollector<DependenciesCollector>();
    telemetry.SetTracer<Tracer, TraceConfig>();
    telemetry.SetSpanExporter<ZipkinTraceExporter>(o => o.ServiceName  = "my-service");
});

Going further, using extension methods for hide some of the wire up:

services.AddOpenTelemetry(telemetry =>
{
    telemetry.UseRequestsCollector();
    telemetry.UseDependenciesCollector();
    telemetry.SetTracer<Tracer, TraceConfig>(); // What is this?
    telemetry.UseZipkin(o => o.ServiceName  = "my-service");
});

@JamesNK
Copy link
Contributor Author

JamesNK commented Oct 2, 2019

telemetry.SetTracer<Tracer, TraceConfig>(); // What is this?

It is different because an argument to TraceConfig's constructor is from DI so TraceConfig can't be newed up. Yeah it sucks.


services.AddOpenTelemetry(telemetry =>
{
    telemetry.AddCollector<RequestsCollector>();
    telemetry.AddCollector<DependenciesCollector>();
    telemetry.SetTracer<Tracer, TraceConfig>();
    telemetry.SetSpanExporter<ZipkinTraceExporter>(o => o.ServiceName  = "my-service");
});

Types on all those constructors take the option objects e.g. RequestCollector.ctor(). Is there a way to make them optional so DI doesn't mind if there is no options object?


services.AddOpenTelemetry(telemetry =>
{
    telemetry.UseRequestsCollector();
    telemetry.UseDependenciesCollector();
    telemetry.SetTracer<Tracer, TraceConfig>(); // What is this?
    telemetry.UseZipkin(o => o.ServiceName  = "my-service");
});

Currently there is zero DI integration throughout the rest of the projects. It is POCO objects with values set via constructors. Extension methods are possible, just means making a choice about:

I've kept the DI dependency isolated to OpenTelemetry.Hosting package. The user experience could be improved with strongly typed registration methods, e.g. AddZipkin(options), but that would require either additional projects depending on DI, or more packages for DI integration, e.g. OpenTelemetry.Exporter.Zipkin.DependencyInjection.

I'm guessing there is a design decision at the moment to avoid a dependency on Microsoft.Extensions.DependencyInjection.

@pakrym
Copy link
Contributor

pakrym commented Oct 2, 2019

First OT needs to decide if they want first-class non-DI tracer setup. Making DI the ultimate composition root would require classes to be designed in a very particular way making non-DI setup prohibitively hard (hello logging).

You can imagine if they have a nice fist class API .Hosting library only has to provide hooks to start/stop collectors and exporters and making tracer resolvable from DI.

OpenTelemetry NO DI

var tracerBuilder = new TracerBuilder();
tracerBuilder.AddRequestCollector(o=> ...);
tracerBuilder.AddDependencyCollector(o=> ...);

tracerBuilder.AddZipkinExporter();

var tracer = tracerBuilder.Build();
await tracer.StartAsync(); // Starts exporters and collectors and whatever else needs explicit starting

Console.WriteLine("Yay");

await tracer.StopAsync();

OpenTelemetry + DI

services.AddOpenTelemetry(tracerBuilder=>
{
	tracerBuilder.AddRequestCollector(o=> ...);
	tracerBuilder.AddDependencyCollector(o=> ...);
	
	tracerBuilder.AddZipkinExporter();
});

// tracerBuilder.Build(), StartAsync/StopAsync is handled for you

/// <inheritdoc />
public override Task<ExportResult> ExportAsync(IEnumerable<Span> batch, CancellationToken cancellationToken)
{
return Task.FromResult(ExportResult.Success);
Copy link

Choose a reason for hiding this comment

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

BTW, I know we already have this problem in another place, would you mind caching task.fromresult - this is new Task every time

Copy link
Contributor

Choose a reason for hiding this comment

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

It should be ValueTask 😄

@lmolkova
Copy link

lmolkova commented Oct 2, 2019

It is different because an argument to TraceConfig's constructor is from DI so TraceConfig can't be newed up. Yeah it sucks.

if you can think of how to change TraceConfig to better suit DI scenarios - go for it.

@lmolkova
Copy link

lmolkova commented Oct 2, 2019

First OT needs to decide if they want first-class non-DI tracer setup. Making DI the ultimate composition root would require classes to be designed in a very particular way making non-DI setup prohibitively hard (hello logging).

Great idea. OTel configuration is too complicated already and it's going to be even worse with #239. We need something to help configure it without DI too. It is nice to make DI work on top of the same configuration sugar.

[Update] and it doesn't have to happen right now

@JamesNK
Copy link
Contributor Author

JamesNK commented Oct 2, 2019

I like #253 (comment)

I think the current approach of using DI to build types, but not integrating with DI, will always result in a bad user experience. The AddOpenTelemetry in this PR is probably the best you can get, and it isn't that great.

What I think you should end up with:

  • Builder types in OpenTelemetry.Abstractions.
  • Extension methods in the various projects to register with the builder, e.g. Zipkin would have AddZipkinExporter(this ITracingBuilder builder, Func<ZipkinOptions> configure) that would set the exporter on the builder.
  • Continue to have OpenTelemetry.Hosting for DI integration and automatic start/stop.

@lmolkova
Copy link

lmolkova commented Oct 3, 2019

I think the current approach of using DI to build types, but not integrating with DI, will always result in a bad user experience. The AddOpenTelemetry in this PR is probably the best you can get, and it isn't that great.

What I think you should end up with:

@JamesNK Thanks! I'll work on this in the next few days.
In the meantime, Things are changing pretty rapidly and a new thing ( TracerFactory) was introduced in #238 that complicates configuration even further and affects this PR as well. I think we can merge this PR (if you can add support for TracerFactory) or wait for non-DI configuration changes first. Let me know how you want to proceed.

@JamesNK JamesNK closed this Oct 12, 2019
Yun-Ting pushed a commit to Yun-Ting/opentelemetry-dotnet that referenced this pull request Oct 13, 2022
* Rename AzureMonitor Extensions

* Add CHANGELOG
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.

Improve dependency injection registration
5 participants