Improve Metrics pull export mode#2389
Conversation
Codecov Report
@@ 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
|
39955ee to
ba04ddf
Compare
|
|
||
| namespace OpenTelemetry | ||
| { | ||
| internal sealed class PullMetricScope : IDisposable |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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...
- 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)- 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).
There was a problem hiding this comment.
Another idea we could explore:
- leverage Event https://docs.microsoft.com/en-us/dotnet/standard/events/
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- why would reader be a separate parameter, rather than part of the options.
- why would we have the
options, readerordering instead ofreader, options = optional. - what if we later we figured that we need to add another parameter?
There was a problem hiding this comment.
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);There was a problem hiding this comment.
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.
pellared
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 reader = new BaseExportingMetricReader(exporter); | ||
|
|
||
| var metricsHttpServer = new PrometheusExporterMetricsHttpServer(exporter); | ||
| metricsHttpServer.Start(); |
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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.
cijothomas
left a comment
There was a problem hiding this comment.
looks very neat!
please update the PR description to reflect new exporter.Collect() style.
updated |
With this change,
meterProvider.ForceFlushwould 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:
And the exporter can trigger Collect by using
exporter.Collect(timeoutMilliseconds). Any direct invocation onreader.Collectandprovider.ForceFlushwould result infalse.