Metrics SDK - zig-o11y/opentelemetry-sdk GitHub Wiki
The Metrics SDK in the Zig OpenTelemetry SDK, located in the src/sdk/metrics module, provides the components needed to consume the Metrics API and turn the instrumentation data points into consumable metrics. It is built from the OpenTelemetry Metrics SDK docs.
Currently, the SDK module is the only one publicly exported in the build.
If structs or functions in other modules need to be called directly by users of the whole library, they need to be declared in sdk.zig
and marked as pub
for it to be usable.
For example, MeterProvider
from the api
module is exported in sdk.zig
(under the same name).
Overview
The design of the sdk
module adheres to the OpenTelemetry specification by creating the same components defined in it:
- View
- MetricReader
- MetricExporter (and the exporters implementations)
Because of the lack of built-in support in the Zig language for interface types, when an interface (in the OOP meaning) is needed, we adopt @fieldParentPtr
to dynamically dispatch the implementation of an interface to a concrete type at runtime.
View
In this component, we have definitions of types used across the SDK to indicate the configuration of how metrics are aggregated and represented before they are exported.
MetricReader
This component is responsible for collecting the metrics from the instruments and forwarding the data to an exporter.
A MetricReader
is always associated with a single MetricExporter
.
Multiple MetricReader
s can be attached to a single MeterProvider
, allowing the same metrics to be aggregated and exported in different ways.
The MetricReader
struct is responsible for fetching the measurements from the instruments, aggregating and compacting them in a way that can be consumed by exporters.
The design of MetricReader is to get an owned copy of measurements from instruments, manipulate them following the "view", and create a new set of data to be passed to the exporter. In this regard, MetricReader needs to free up the memory allocated for the data read from instruments once they are processed.
MetricExporter
In the OpenTelemetry specification, this component is the interface defining how metrics are exported.
Exported means that they are converted from the internal SDK data structures to some other form that can be consumed outside of the SDK.
We use the MetricExporter
struct to achieve this step, synchronize shutdown and cleanup of data, and pass configuration entries such as aggregation and temporality back to the MetricReader.
Users can plug in their custom exporter, if needed, but they are encouraged to use the existing exporters through the factory methods of MetricExporter
.
[!IMPORTANT] MetricExporter owns the
Measurements
slice passed as theexportBatch
argument. This means it needs to clear the memory allocated for the slice (and its content).
MetricExporter
uses @fieldParentPtr
mentioned above to overcome the lack of interfaces:
- the
ExporterImpl
struct defines the interface - other types implementing the interface have to have an
exporter
field of type, forwarding the export logic to a member function - the field of the struct is passed as an argument to
MetricExporter.new()
to build it - when
MetricExporter
calls theExporterImpl
method, a pointer to its parent struct is found and the dispatch happens
Building a custom exporter
A user willing to build a custom exporter would have to rely on the same ExporterImpl
struct paired with @fieldParentPtr
.
Imagine a ChaosExporter
that scrambles the metrics. It would be built as such
pub const ChaosExporter = struct {
exporter: ExporterImpl,
fn exportBatch(iface: *ExporterImpl, metrics: []Measurements) MetricReadError!void {
// Get a pointer to the instance of the struct that implements the interface.
const self: *Self = @fieldParentPtr("exporter", iface);
// apply chaos
for(metrics, 0..) |m, i| {
...
}
}
pub fn init() ChaosExporter {
return ChaosExporter{ .exporter = ExporterImpl{
.exportFn = exportBatch,
}};
}
}
Then it would be usable as such:
const ce = ChaosExporter.init();
const me = try sdk.MetricExporter.new(allocator, &ce.exporter)
defer me.shutdown()
const mr = try sdk.MetricReader.init(allocator, me)
defer mr.shutdown()