Skip to content

feat(metrics): add public constructors for metric data types#3489

Open
darklight3it wants to merge 6 commits into
open-telemetry:mainfrom
darklight3it:add-public-constructor-to-metric-data-types
Open

feat(metrics): add public constructors for metric data types#3489
darklight3it wants to merge 6 commits into
open-telemetry:mainfrom
darklight3it:add-public-constructor-to-metric-data-types

Conversation

@darklight3it

@darklight3it darklight3it commented May 3, 2026

Copy link
Copy Markdown

Fixes #3469

Changes

Added public constructors to metric data types, allowing users to use the public PushMetricExporter::export.

Rationale:

  • Types with one or more optional fields use the builder pattern, where the builder explicitly lists mandatory fields. This matches the established pattern in opentelemetry-sdk, opentelemetry-otlp, and other crates.
  • Types with only required fields use a new method.

Summary:

Type Constructor Required fields Optional (with_*) fields
ResourceMetrics builder() resource, scope_metrics
ScopeMetrics builder() scope, metrics
Metric builder(name, data) name, data description, unit
Gauge<T> builder(data_points, time) data_points, time start_time
GaugeDataPoint<T> builder(value) value attributes, exemplars
Sum<T> new(...) data_points, temporality, is_monotonic, start_time, time
SumDataPoint<T> builder(value) value attributes, exemplars
Histogram<T> new(...) data_points, temporality, start_time, time
HistogramDataPoint<T> builder(count, sum, bounds, bucket_counts) count, sum, bounds, bucket_counts attributes, min, max, exemplars
ExponentialHistogram<T> new(...) data_points, temporality, start_time, time
ExponentialHistogramDataPoint<T> builder(count, sum, scale, zero_count, positive_bucket, negative_bucket) count, sum, scale, zero_count, positive_bucket, negative_bucket attributes, min, max, zero_threshold, exemplars
ExponentialBucket new(offset, counts) offset, counts
Exemplar<T> builder(value, time) value, time filtered_attributes, span_id, trace_id

Example usage:

use opentelemetry::{InstrumentationScope, KeyValue};
use opentelemetry::time::now;
use opentelemetry_sdk::metrics::data::*;
use opentelemetry_sdk::metrics::{Temporality, PushMetricExporter};
use opentelemetry_sdk::Resource;

let gauge_dp = GaugeDataPoint::builder(42.0_f64)
    .with_attributes(vec![KeyValue::new("host", "localhost")])
    .build();

let gauge = Gauge::builder(vec![gauge_dp], now()).build();

let metric = Metric::builder("cpu.usage", AggregatedMetrics::F64(MetricData::Gauge(gauge)))
    .with_description("CPU usage percentage")
    .with_unit("%")
    .build();

let scope_metrics = ScopeMetrics::builder()
    .with_scope(InstrumentationScope::builder("my-library").build())
    .with_metrics(vec![metric])
    .build();

let resource_metrics = ResourceMetrics::builder()
    .with_resource(Resource::builder().with_service_name("my-service").build())
    .with_scope_metrics(vec![scope_metrics])
    .build();

exporter.export(&resource_metrics).await?;

Merge requirement checklist

  • CONTRIBUTING guidelines followed
  • Unit tests added/updated (if applicable)
  • Appropriate CHANGELOG.md files updated for non-trivial, user-facing changes
  • Changes in public API reviewed (if applicable)

@linux-foundation-easycla

linux-foundation-easycla Bot commented May 3, 2026

Copy link
Copy Markdown

CLA Not Signed

@darklight3it darklight3it changed the title Add public constructors for metric data types feat(metrics): add public constructors for metric data types May 3, 2026
@codecov

codecov Bot commented May 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.08884% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.6%. Comparing base (ec289cb) to head (dcbc2e4).

Files with missing lines Patch % Lines
opentelemetry-sdk/src/metrics/data/mod.rs 99.0% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #3489     +/-   ##
=======================================
+ Coverage   82.8%   83.6%   +0.7%     
=======================================
  Files        130     130             
  Lines      27289   27728    +439     
=======================================
+ Hits       22622   23186    +564     
+ Misses      4667    4542    -125     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@darklight3it darklight3it force-pushed the add-public-constructor-to-metric-data-types branch from f3fc113 to d3e1646 Compare May 3, 2026 12:30
@darklight3it darklight3it marked this pull request as ready for review May 5, 2026 09:47
@darklight3it darklight3it requested a review from a team as a code owner May 5, 2026 09:47
@lalitb

lalitb commented May 6, 2026

Copy link
Copy Markdown
Member

@darklight3it - Can you clarify the intended use case for constructing ResourceMetrics and calling PushMetricExporter::export() directly?

In the normal metrics pipeline, instrumentation libraries create measurements through instruments, and the SDK/MetricReader produces ResourceMetrics for the exporter. So the example shared in the desc looks like a lower-level/exporter use case rather than normal instrumentation.

Is the target use case pre-aggregated metrics from an external source, or directly sending measurements to an exporter while bypassing SDK aggregation? If it is the latter, I am not sure ResourceMetrics constructors are the right API shape, since these types represent already-aggregated metric points, not raw measurements. The spec direction for third-party/pre-processed metric sources seems closer to a MetricProducer plugged into a MetricReader.

@darklight3it

Copy link
Copy Markdown
Author

Yes, the target use case is pre-aggregated metrics from an external source. The aggregation is already done, the Rust side only needs to serialize and transport the resulting ResourceMetrics via OTLP.

The challenge I'm running into is that MetricExporter::export() is public and intended to be callable externally, but its argument (ResourceMetrics) can't be constructed outside the crate.

This makes it difficult to use the exporter trait independently of the SDK's internal reader pipeline. I think opening up ResourceMetrics would better support the modularity that motivated having opentelemetry-otlp as a separate crate and would bring metrics in line with spans and logs, where SpanData and LogRecord already have public fields and work well for custom exporter implementations.

@lalitb

lalitb commented May 7, 2026

Copy link
Copy Markdown
Member

Thanks, that use case makes sense.

I think this is closer to the spec’s MetricProducer path, though. MetricProducer is meant for third-party/pre processed metric sources, and it plugs into a MetricReader. So the shape would be more like: external aggregated source -> MetricProducer -> MetricReader -> exporter.

That makes me hesitant to expose constructors for the full ResourceMetrics tree and encourage calling PushMetricExporter::export() directly. It feels like we would be making a lot of SDK metric data internals part of the stable public construction API. I’d prefer we discuss a smaller API for this use case, maybe MetricProducer-style, instead of opening up all these data types as-is.

@darklight3it

darklight3it commented May 7, 2026

Copy link
Copy Markdown
Author

Thanks @lalitb for the thoughtful feedback, I appreciate the discussion.

On MetricProducer:

On the concern about opening up the full ResourceMetrics tree:

  • The builder doesn't expose fields directly. It keeps internal data behind public methods, the same pattern as the existing getters (and in other types throughout the repo). The stability surface is symmetric with what's already committed: export() is public, the getters are public, ResourceMetrics::Default() already allows external construction. The builder just lets you construct a populated instance.

  • The PR does touch nested types (ScopeMetrics, Metric, etc.) with the same builder pattern. But the same argument applies uniformly: each of these types already has public getters that expose their shape. The builders don't add new surface area, they provide the construction counterpart to what's already readable. New fields and new subtypes should already be maintained like this for the reading part.

  • If maintenance is a concern, a derive macro (like typed-builder) could auto-generate the builders from the struct definitions, keeping them in sync automatically. I considered that approach but chose hand-written builders to avoid adding a proc-macro dependency to the crate, which I think is the right choice for a foundational library like this.

@lalitb

lalitb commented May 8, 2026

Copy link
Copy Markdown
Member

I think MetricProducer is still the closer conceptual fit - and it would be better to invest in bringing this feature back.

The “pull” part there is only inside the SDK: the MetricReader asks the producer for already-aggregated data. It does not mean the final export has to be pull-based. The final exporter can still be a push exporter sending OTLP.

So the shape would be: external pre-aggregated source -> MetricProducer -> MetricReader -> PushMetricExporter.

For the immediate-export case, we may need an API that lets the reader collect/export on demand, but I do not think that means PushMetricExporter::export should become the public ingestion path or that we should expose builders for the whole ResourceMetrics tree.

@darklight3it

darklight3it commented May 9, 2026

Copy link
Copy Markdown
Author

I understand the preference for an internal path, but if I'm reading the proposed workflow correctly, the changes in this PR are still necessary for that to work.

// simplified MetricProducer shape to illustrate the point
struct MyProducer { buffer: Mutex<Vec<ScopeMetrics>> }

impl MetricProducer for MyProducer {
    fn produce(&self) -> Vec<ScopeMetrics> {
         self.buffer.lock().unwrap().drain(..).collect()
     }
 }

fn receive_external_metrics(data: ExternalData) {
     let scope_metrics: Vec<ScopeMetrics> = data.into(); // needs constructors for
                                                          // ScopeMetrics, Metric,
                                                          // Sum/Gauge/Histogram,
                                                          // DataPoint
     PRODUCER.buffer.lock().unwrap().extend(scope_metrics);
     READER.force_collect(); // on-demand collect to control when data is flushed
 }

Either way, users will need a supported way to create these types from external data, which means exposing some form of construction contract. We could introduce a parallel set of public DTO types and map them to the ResourceMetrics tree internally, but given that these types are already partially public (getters, Default, trait signatures), creating a separate anticorruption layer won't help without breaking the contract and closing these types.

The same reasoning applies to the PushMetricExporter. It's public but currently unusable since its argument can't be constructed. If the intent is for it not to be called directly, it may be worth reconsidering whether it should remain public in a separate crate.

@cijothomas

cijothomas commented May 11, 2026

Copy link
Copy Markdown
Member

The same reasoning applies to the PushMetricExporter. It's public but currently unusable since its argument can't be constructed. If the intent is for it not to be called directly, it may be worth reconsidering whether it should remain public in a separate crate.

The PushMetricExporter trait is public because users need to implement it (write custom exporters), not because they are expected to call export() directly. The SDK's PeriodicReader constructs ResourceMetrics internally and calls export() on the exporter. There are already several implementations across crates (opentelemetry-otlp, opentelemetry-stdout, InMemoryMetricExporter) — the trait living in a separate crate is by design, not an oversight. Implementors receive ResourceMetrics; they don't need to construct it.

That said, I do acknowledge that when we bring back MetricProducer, we may well end up needing public constructors for some of these metric data types — a producer would need to build ScopeMetrics, Metric, data points, etc. It's just too early to commit to that API surface right now before MetricProducer itself is designed and landed.

Would you have bandwidth to help bring MetricProducer back? That would be the right foundation for your use case and would naturally inform which constructors are actually needed.

@darklight3it

darklight3it commented May 27, 2026

Copy link
Copy Markdown
Author

Hello @cijothomas,

thanks for the answer and sorry for the delay, my last two weeks were very intense.

That said, I do acknowledge that when we bring back MetricProducer, we may well end up needing public constructors for some of these metric data types — a producer would need to build ScopeMetrics, Metric, data points, etc. It's just too early to commit to that API surface right now before MetricProducer itself is designed and landed.
Would you have bandwidth to help bring MetricProducer back? That would be the right foundation for your use case and would naturally inform which constructors are actually needed.

I'm happy to help bring MetricProducer back if it unblocks my use case. But before I commit, I want to understand the intended architecture better. Unless you're thinking of duplicating those types or introducing a breaking change, the moment those types get public constructors PushMetricExporter also becomes automatically a fully usable extension point, falling back to the case I originally presented. What options were you thinking of?

Secondly, my use case requires that all of those types are constructable. As I said before, I already have the entire payload constructed and aggregated, I only need to map it to those types and export it.

@michaelvanstraten

Copy link
Copy Markdown

I would like to use the opentelemetry-otlp exporters outside of the SDK context (e.g. not have them attached to a Log/Metrics processor).

Going this way I would need to make SdkLogRecord publicly construct-able, feels kind of weird to break the Logger API that way.

@darklight3it would it work for you if we added a proto_export methods to the opentelemetry_otlp exporters which would accept a type from opentelemetry-proto (e.g. https://docs.rs/opentelemetry-proto/latest/opentelemetry_proto/tonic/metrics/v1/struct.ResourceMetrics.html)?

@darklight3it

darklight3it commented Jun 20, 2026

Copy link
Copy Markdown
Author

Hello @michaelvanstraten,

Thanks for your answer ❤️.

That would work, however if a user needs to re-derive the entire model from opentelemetry-proto types, it becomes more convenient to just reimplement a custom OTLP exporter from scratch rather than using the one provided out of the box.

At that point the proto_export method doesn't save much, you're already doing half of the job rather than using a pluggable component and its interface.

@michaelvanstraten

Copy link
Copy Markdown

That would work, however if a user needs to re-derive the entire model from opentelemetry-proto types, it becomes more convenient to just reimplement a custom OTLP exporter from scratch rather than using the one provided out of the box.

Reimplementing a custom OTLP exporter is what I ended up doing (HTTP + GRPC under 1000 lines clean). However, the modeling from opentelemetry-proto types is not necessary since there are predefined transforms in the crate that already do this for you e.g. you can just call .into() on a ResourceMetric to get the proto type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add public constructors for metric data types

4 participants