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 MetricReaders 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 the exportBatch 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 the ExporterImpl 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()