-
Notifications
You must be signed in to change notification settings - Fork 790
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
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.
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) |
There was a problem hiding this comment.
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...
- 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.
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:
- leverage Event https://docs.microsoft.com/en-us/dotnet/standard/events/
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- why would reader be a separate parameter, rather than part of the options.
- why would we have the
options, reader
ordering instead ofreader, options = optional
. - what if we later we figured that we need to add another parameter?
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CodeBlanch @pellared PTAL.
There was a problem hiding this comment.
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.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
updated |
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:
And the exporter can trigger Collect by using
exporter.Collect(timeoutMilliseconds)
. Any direct invocation onreader.Collect
andprovider.ForceFlush
would result infalse
.