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

Improve Metrics pull export mode #2389

Merged
merged 10 commits into from
Sep 22, 2021

Conversation

reyang
Copy link
Member

@reyang reyang commented Sep 21, 2021

With this change, meterProvider.ForceFlush would return false if the underlying exporter ONLY supports pull mode.

In pull exporters (e.g. PrometheusExporter), we can still use the following mechanism to pull metrics.

A basic pull-only exporter would look like this:

[ExportModes(ExportModes.Pull)]
private class PullOnlyMetricExporter : BaseExporter<Metric>, IPullMetricExporter
{
    private Func<int, bool> funcCollect;

    public Func<int, bool> Collect
    {
        get => this.funcCollect;
        set { this.funcCollect = value; }
    }

    public override ExportResult Export(in Batch<Metric> batch)
    {
        return ExportResult.Success;
    }
}

And the exporter can trigger Collect by using exporter.Collect(timeoutMilliseconds). Any direct invocation on reader.Collect and provider.ForceFlush would result in false.

// in the exporter code where we handle scraper
exporter.Collect(timeoutMilliseconds);

@reyang reyang requested a review from a team September 21, 2021 03:22
@codecov
Copy link

codecov bot commented Sep 21, 2021

Codecov Report

Merging #2389 (d64dec7) into main (032f22d) will increase coverage by 0.20%.
The diff coverage is 96.42%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2389      +/-   ##
==========================================
+ Coverage   79.99%   80.20%   +0.20%     
==========================================
  Files         231      232       +1     
  Lines        7464     7492      +28     
==========================================
+ Hits         5971     6009      +38     
+ Misses       1493     1483      -10     
Impacted Files Coverage Δ
...OpenTelemetry/Metrics/BaseExportingMetricReader.cs 85.10% <94.11%> (+11.77%) ⬆️
src/OpenTelemetry/Metrics/PullMetricScope.cs 100.00% <100.00%> (ø)
...Zipkin/Implementation/ZipkinExporterEventSource.cs 63.63% <0.00%> (-9.10%) ⬇️
src/OpenTelemetry/Metrics/MeterProviderSdk.cs 91.08% <0.00%> (+0.99%) ⬆️
...c/OpenTelemetry/Metrics/MeterProviderExtensions.cs 22.72% <0.00%> (+22.72%) ⬆️
src/OpenTelemetry/Metrics/ExportModesAttribute.cs 100.00% <0.00%> (+100.00%) ⬆️

@reyang reyang closed this Sep 21, 2021
@reyang reyang reopened this Sep 21, 2021

namespace OpenTelemetry
{
internal sealed class PullMetricScope : IDisposable
Copy link
Member

Choose a reason for hiding this comment

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

I like this as a way for exporter to tell Collect that it wants to allow pull. Consistent with how we do the instrumentation suppression.

/// <remarks>
/// This function guarantees thread-safety.
/// </remarks>
public static bool Collect(this BaseExporter<Metric> exporter, BaseExportingMetricReader reader, int timeoutMilliseconds = Timeout.Infinite)
Copy link
Member

Choose a reason for hiding this comment

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

I was chatting to @reyang about this a bit on Slack. Nothing against the extension, just not sure how Exporter will get the second param (MetricReader). Also seems a bit odd the extension is on Exporter, but really only Reader is used.

A couple of ideas for exposing MetricReader to exporter...

  1. Don't. Just feed it to Prometheus where we need it. If some other exporter comes along that needs to call collect, we tackle it then
public PrometheusExporter(PrometheusExporterOptions options, MetricReader parentMetricReader)
  1. I guess @alanwest tried to do some base classes but ran into issues with InMemoryExporter<T>. What if we went with an interface? Closest thing C# has to composition.
public interface ICollectingExporter
{
   MetricReader ParentMetricReader { get; set; } 
}

public class PrometheusExporter : ICollectingExporter
{
   public MetricReader ParentMetricReader { get; set; } 
}

During MeterProvider build-up we then just do an is check and set the ParentMetricReader on any exporter that implements the interface (basically if exporter asks for it).

Copy link
Member Author

Choose a reason for hiding this comment

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

Another idea we could explore:

  1. leverage Event https://docs.microsoft.com/en-us/dotnet/standard/events/

Copy link
Member

Choose a reason for hiding this comment

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

Please take my comment from a distance. Just learning the API. But maybe some fresh eyes will help somehow.

This extension method design is strange. Does it mean that the BaseExporter<Metric> has to be coupled with BaseExportingMetricReader? If so then maybe it will be handy to create BaseMetricExporter?

Regarding @CodeBlanch I definitely prefer approach 1 (composition) as it leads to less coupling. Also, I consider interface getters and setters as a code smell.

Not sure how we would like to use event but personally I prefer IObservable<T> as it is easier to compose, works better with extension methods etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Option 1 is on the bottom of my list in terms of preference, I even suspect if we could do that (reader.ctor needs exporter, how could exporter.ctor take reader?). I guess we might be able to tweak it and make it work, but I have strong feeling that the exporter.ctor(options, reader) is a bad design:

  1. why would reader be a separate parameter, rather than part of the options.
  2. why would we have the options, reader ordering instead of reader, options = optional.
  3. what if we later we figured that we need to add another parameter?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've taken option 2 from @CodeBlanch.

A basic pull-only exporter would look like this:

[ExportModes(ExportModes.Pull)]
private class PullOnlyMetricExporter : BaseExporter<Metric>, IPullMetricExporter
{
    private Func<int, bool> funcCollect;

    public Func<int, bool> Collect
    {
        get => this.funcCollect;
        set { this.funcCollect = value; }
    }

    public override ExportResult Export(in Batch<Metric> batch)
    {
        return ExportResult.Success;
    }
}

And the exporter can trigger Collect by using exporter.Collect(timeoutMilliseconds). Any direct invocation on reader.Collect and provider.ForceFlush would result in false.

// in the exporter code where we handle scraper
exporter.Collect(timeoutMilliseconds);

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

@pellared pellared Sep 22, 2021

Choose a reason for hiding this comment

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

After looking again, I agree that option 1 would be worse.

The current code is OK. I will create a PR, if I come up with anything potentially better.

Copy link
Member

@pellared pellared left a comment

Choose a reason for hiding this comment

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

My review probably does not bring much value. Only subjective comments.

/// <remarks>
/// This function guarantees thread-safety.
/// </remarks>
public static bool Collect(this BaseExporter<Metric> exporter, BaseExportingMetricReader reader, int timeoutMilliseconds = Timeout.Infinite)
Copy link
Member

Choose a reason for hiding this comment

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

Please take my comment from a distance. Just learning the API. But maybe some fresh eyes will help somehow.

This extension method design is strange. Does it mean that the BaseExporter<Metric> has to be coupled with BaseExportingMetricReader? If so then maybe it will be handy to create BaseMetricExporter?

Regarding @CodeBlanch I definitely prefer approach 1 (composition) as it leads to less coupling. Also, I consider interface getters and setters as a code smell.

Not sure how we would like to use event but personally I prefer IObservable<T> as it is easier to compose, works better with extension methods etc.


var metricReader = new BaseExportingMetricReader(exporter);
exporter.CollectMetric = metricReader.Collect;
var reader = new BaseExportingMetricReader(exporter);

var metricsHttpServer = new PrometheusExporterMetricsHttpServer(exporter);
metricsHttpServer.Start();
Copy link
Member Author

@reyang reyang Sep 22, 2021

Choose a reason for hiding this comment

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

@alanwest I noticed this while changing the code.

I think we might run into some race condition - if a scraper happens to hit the HTTP server before we could add the reader, what would happen (I guess we will hit exception, which turns into HTTP 500)? I haven't looked into the HTTP server logic.

I think it might be OKAY. A better version could be - we only start the HTTP server once the exporter/reader are fully ready and both are hooked up to the provider.

Copy link
Member

Choose a reason for hiding this comment

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

if the reader is not ready, we could modify the http server to still return 200, but with empty metric.
try
{
this.exporter.Collect(Timeout.Infinite)
}
catch(Exporter/Reader not readyexception)
{
exporter.BatchMetrics = default.
}

Or we can wait for some signal before starting HTTP Server.
Will track this as a separate follow up once this PR is merged.

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.

looks very neat!
please update the PR description to reflect new exporter.Collect() style.

@reyang
Copy link
Member Author

reyang commented Sep 22, 2021

looks very neat!
please update the PR description to reflect new exporter.Collect() style.

updated

@cijothomas cijothomas merged commit 15981df into open-telemetry:main Sep 22, 2021
@reyang reyang deleted the reyang/pull-mode branch September 22, 2021 20:09
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.

4 participants