- Building your own exporter
- Building your own reader
- Building your own exemplar reservoir
- Building your own resource detector
- References
OpenTelemetry .NET SDK has provided the following built-in metric exporters:
Custom exporters can be implemented to send telemetry data to places which are not covered by the built-in exporters:
- Exporters should derive from
OpenTelemetry.BaseExporter<Metric>
(which belongs to the OpenTelemetry package) and implement theExport
method. - Exporters can optionally implement the
OnShutdown
method. - Exporters should not throw exceptions from
Export
andOnShutdown
. - Exporters are responsible for any retry logic needed by the scenario. The SDK does not implement any retry logic.
- Exporters should avoid generating telemetry and causing live-loop, this can be
done via
OpenTelemetry.SuppressInstrumentationScope
. - Exporters receives a batch of
Metric
, and eachMetric
can contain 1 or moreMetricPoint
s. The exporter should perform all actions (e.g. serializing etc.) with theMetric
s andMetricsPoint
s in the batch before returning control fromExport
, once the control is returned, the exporter can no longer make any assumptions about the state of the batch or anything inside it. - Exporters should use
ParentProvider.GetResource()
to get theResource
associated with the provider.
class MyExporter : BaseExporter<Metric>
{
public override ExportResult Export(in Batch<Metric> batch)
{
using var scope = SuppressInstrumentationScope.Begin();
foreach (var metric in batch)
{
Console.WriteLine($"Export: {metric.metric}");
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
Console.WriteLine($"Export: {metricPoint.StartTime}");
}
}
return ExportResult.Success;
}
}
A demo exporter which simply writes metric name, metric point start time and tags to the console is shown here.
Apart from the exporter itself, you should also provide extension methods as
shown here. This allows users to add the Exporter
to the MeterProvider
as shown in the example here.
Not supported.
Note
ExemplarReservoir
is an experimental API only available in pre-release
builds. For details see:
OTEL1004. Please provide
feedback
to help inform decisions about what should be exposed stable and when.
Custom ExemplarReservoirs
can be implemented to control how Exemplar
s are recorded for a metric:
ExemplarReservoir
s should derive fromFixedSizeExemplarReservoir
(which belongs to the OpenTelemetry package) and implement theOffer
methods.- The
FixedSizeExemplarReservoir
constructor accepts acapacity
parameter to control the number ofExemplar
s which may be recorded by theExemplarReservoir
. - The
virtual
OnCollected
method is called after theExemplarReservoir
collection operation has completed and may be used to implement cleanup or reset logic. - The
bool
ResetOnCollect
property onExemplarReservoir
is set totrue
when delta aggregation temporality is used for the metric using theExemplarReservoir
. - The
Offer
andCollect
ExemplarReservoir
methods are called concurrently by the OpenTelemetry SDK. As such any state required by customExemplarReservoir
implementations needs to be managed using appropriate thread-safety/concurrency mechanisms (lock
,Interlocked
, etc.). - Custom
ExemplarReservoir
implementations MUST NOT throw exceptions. Exceptions thrown in custom implementations MAY lead to unreleased locks and undefined behaviors.
The following example demonstrates a custom ExemplarReservoir
implementation
which records Exemplar
s for measurements which have the highest value. When
delta aggregation temporality is used the recorded Exemplar
will be the
highest value for a given collection cycle. When cumulative aggregation
temporality is used the recorded Exemplar
will be the highest value for the
lifetime of the process.
class HighestValueExemplarReservoir : FixedSizeExemplarReservoir
{
private readonly object lockObject = new();
private long? previousValueLong;
private double? previousValueDouble;
public HighestValueExemplarReservoir()
: base(capacity: 1)
{
}
public override void Offer(in ExemplarMeasurement<long> measurement)
{
if (!this.previousValueLong.HasValue || measurement.Value > this.previousValueLong.Value)
{
lock (this.lockObject)
{
if (!this.previousValueLong.HasValue || measurement.Value > this.previousValueLong.Value)
{
this.UpdateExemplar(0, in measurement);
this.previousValueLong = measurement.Value;
}
}
}
}
public override void Offer(in ExemplarMeasurement<double> measurement)
{
if (!this.previousValueDouble.HasValue || measurement.Value > this.previousValueDouble.Value)
{
lock (this.lockObject)
{
if (!this.previousValueDouble.HasValue || measurement.Value > this.previousValueDouble.Value)
{
this.UpdateExemplar(0, in measurement);
this.previousValueDouble = measurement.Value;
}
}
}
}
protected override void OnCollected()
{
if (this.ResetOnCollect)
{
lock (this.lockObject)
{
this.previousValueLong = null;
this.previousValueDouble = null;
}
}
}
}
Custom ExemplarReservoirs can be configured using the View API. For details see: Change the ExemplarReservoir.