Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .chloggen/exponential_metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
component: cmd/mdatagen

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Adding support for histogram aggregation in `mdatagen` definitions.

# One or more tracking issues or pull requests related to the change
issues: [13368]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: |
Histogram aggregation type introduced in this release in order to define the type of histogram to be used.
Defaults to "explicit" (explicit bucket histogram). Use "exponential" for exponential bucket histograms.
When set, a Views() function is generated and must be passed to the SDK MeterProvider.
The `max_size` and `max_scale` fields control the bucket resolution (scale must be in [0, 20]).
`max_size`: defaults to 160 buckets
`max_scale`: defaults to 20

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]
8 changes: 8 additions & 0 deletions cmd/mdatagen/internal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,14 @@ func getTemplateFuncMap(md Metadata, importRootPath string) template.FuncMap {
}
return false
},
"hasExponentialHistogram": func(metrics map[MetricName]Metric) bool {
for _, m := range metrics {
if m.Histogram != nil && m.Histogram.IsExponential() {
return true
}
}
return false
},
"stringsJoin": strings.Join,
"stringsSplit": strings.Split,
"trimRight": strings.TrimRight,
Expand Down
52 changes: 52 additions & 0 deletions cmd/mdatagen/internal/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ func TestRunContents(t *testing.T) {
yml: "with_invalid_config_ref.yaml",
wantRunErr: true,
},
{
yml: "with_exponential_histogram_telemetry.yaml",
wantStatusGenerated: true,
wantTelemetryGenerated: true,
wantReadmeGenerated: true,
wantComponentTestGenerated: true,
wantLogsGenerated: true,
},
{
yml: "versioned_metric.yaml",
wantStatusGenerated: true,
Expand Down Expand Up @@ -1662,6 +1670,50 @@ func Tracer(settings component.TelemetrySettings) trace.Tracer {
}
}

func TestGenerateTelemetryMetadata_ExponentialHistogram(t *testing.T) {
unit := "By"
md := Metadata{
Type: "foo",
Status: &Status{
Stability: map[component.StabilityLevel][]string{component.StabilityLevelBeta: {"metrics"}},
Distributions: []string{"contrib"},
Class: "receiver",
},
Telemetry: Telemetry{
Metrics: map[MetricName]Metric{
"request_size": {
Signal: Signal{
Enabled: true,
Description: "Size of requests",
Stability: component.StabilityLevelAlpha,
},
Unit: &unit,
Histogram: &Histogram{
Aggregation: HistogramAggregationExponential,
MaxSize: 160,
MaxScale: 10,
},
},
},
},
}

tmpdir := t.TempDir()
err := generateFile("templates/telemetry.go.tmpl",
filepath.Join(tmpdir, "generated_telemetry.go"), md, "metadata", "go.opentelemetry.io/collector")
require.NoError(t, err)

actual, err := os.ReadFile(filepath.Clean(filepath.Join(tmpdir, "generated_telemetry.go")))
require.NoError(t, err)

content := string(actual)
assert.Contains(t, content, `sdkmetric "go.opentelemetry.io/otel/sdk/metric"`)
assert.Contains(t, content, "func Views()")
assert.Contains(t, content, "sdkmetric.AggregationBase2ExponentialHistogram")
assert.Contains(t, content, "MaxSize: 160")
assert.Contains(t, content, "MaxScale: 10")
}

func TestGenerateConfigGoStruct_GeneratesTestFile(t *testing.T) {
root := t.TempDir()
outputDir := filepath.Join(root, "shortname")
Expand Down
14 changes: 14 additions & 0 deletions cmd/mdatagen/internal/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,20 @@ func TestLoadMetadata(t *testing.T) {
Boundaries: []float64{1, 10, 100},
},
},
"request_duration_exponential": {
Signal: Signal{
Enabled: true,
Stability: component.StabilityLevelAlpha,
Description: "Duration of request (exponential histogram)",
},
Unit: strPtr("s"),
Histogram: &Histogram{
MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeDouble},
Aggregation: HistogramAggregationExponential,
MaxSize: 320,
MaxScale: 10,
},
},
"process_runtime_total_alloc_bytes": {
Signal: Signal{
Enabled: true,
Expand Down
12 changes: 12 additions & 0 deletions cmd/mdatagen/internal/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ func TestValidate(t *testing.T) {
name: "testdata/entity_metrics_events_valid.yaml",
wantErr: "",
},
{
name: "testdata/with_exponential_histogram_telemetry.yaml",
wantErr: "",
},
{
name: "testdata/invalid_exponential_histogram_with_boundaries.yaml",
wantErr: "bucket_boundaries must not be set when aggregation is \"exponential\"",
},
{
name: "testdata/invalid_histogram_aggregation.yaml",
wantErr: "invalid aggregation \"invalid_aggregation_type\"",
},
{
name: "testdata/no_stability_noncomponent.yaml",
wantErr: "",
Expand Down
61 changes: 60 additions & 1 deletion cmd/mdatagen/internal/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ func (m *Metric) validate(metricName MetricName, semConvVersion string) error {
if m.Gauge != nil {
errs = errors.Join(errs, m.Gauge.Validate())
}
if m.Histogram != nil {
errs = errors.Join(errs, m.Histogram.Validate())
}
if m.SemanticConvention != nil {
if err := validateSemConvMetricURL(m.SemanticConvention.SemanticConventionRef, semConvVersion, string(metricName)); err != nil {
errs = errors.Join(errs, err)
Expand Down Expand Up @@ -385,16 +388,72 @@ func (d *Sum) IsAsync() bool {

var _ MetricData = (*Histogram)(nil)

// HistogramAggregation specifies the aggregation type for a histogram metric.
type HistogramAggregation string

const (
// HistogramAggregationExplicit uses explicit, predefined bucket boundaries.
// This is the default when no aggregation is specified.
HistogramAggregationExplicit HistogramAggregation = "explicit"
// HistogramAggregationExponential uses base2 exponential bucket boundaries,
// providing adaptive bucketing and better visibility into long-tail distributions.
HistogramAggregationExponential HistogramAggregation = "exponential"
)

type Histogram struct {
AggregationTemporality `mapstructure:"aggregation_temporality"`
Mono `mapstructure:",squash"`
MetricValueType `mapstructure:"value_type"`
MetricInputType `mapstructure:",squash"`
Async bool `mapstructure:"async,omitempty"`
Boundaries []float64 `mapstructure:"bucket_boundaries"`
// Aggregation specifies the histogram aggregation type. Defaults to "explicit".
// Use "exponential" for exponential bucket histograms.
Aggregation HistogramAggregation `mapstructure:"aggregation,omitempty"`
// MaxSize is the maximum number of buckets for exponential histograms.
// Defaults to 160 when unset (0). Only valid when aggregation is "exponential".
MaxSize int `mapstructure:"max_size,omitempty"`
// MaxScale is the maximum resolution scale for exponential histograms.
// Must be in [0, 20]. Defaults to 20 when unset (0). Only valid when aggregation is "exponential".
MaxScale int `mapstructure:"max_scale,omitempty"`
}

// IsExponential reports whether this histogram uses exponential bucket aggregation.
func (d *Histogram) IsExponential() bool {
return d.Aggregation == HistogramAggregationExponential
}

// Validate checks that the histogram configuration is consistent.
func (d *Histogram) Validate() error {
switch d.Aggregation {
case "", HistogramAggregationExplicit, HistogramAggregationExponential:
// valid
default:
return fmt.Errorf("invalid aggregation %q: must be %q or %q",
d.Aggregation, HistogramAggregationExplicit, HistogramAggregationExponential)
}
if d.IsExponential() && len(d.Boundaries) > 0 {
return errors.New("bucket_boundaries must not be set when aggregation is \"exponential\"")
}
if d.MaxSize != 0 && !d.IsExponential() {
return errors.New("max_size is only valid when aggregation is \"exponential\"")
}
if d.MaxScale != 0 && !d.IsExponential() {
return errors.New("max_scale is only valid when aggregation is \"exponential\"")
}
if d.MaxSize < 0 {
return fmt.Errorf("max_size must be a positive integer, got %d", d.MaxSize)
}
if d.MaxScale < 0 || d.MaxScale > 20 {
return fmt.Errorf("max_scale must be between 0 and 20, got %d", d.MaxScale)
}
return nil
}

func (d *Histogram) Type() string {
if d.IsExponential() {
return "ExponentialHistogram"
}
return "Histogram"
}

Expand All @@ -408,7 +467,7 @@ func (d *Histogram) HasAggregated() bool {

func (d *Histogram) Instrument() string {
instrumentName := cases.Title(language.English).String(d.BasicType())
return instrumentName + d.Type()
return instrumentName + "Histogram"
}

// Unmarshal is a custom unmarshaler for histogram. Needed mostly to avoid MetricValueType.Unmarshal inheritance.
Expand Down
68 changes: 68 additions & 0 deletions cmd/mdatagen/internal/metric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,73 @@ func TestMetricInputTypeValidate(t *testing.T) {
}
}

func TestHistogramValidate(t *testing.T) {
tests := []struct {
name string
h Histogram
wantErr string
}{
{
name: "valid explicit default",
h: Histogram{},
},
{
name: "valid explicit",
h: Histogram{Aggregation: HistogramAggregationExplicit},
},
{
name: "valid exponential",
h: Histogram{Aggregation: HistogramAggregationExponential, MaxSize: 160, MaxScale: 10},
},
{
name: "invalid aggregation",
h: Histogram{Aggregation: "invalid"},
wantErr: "invalid aggregation",
},
{
name: "exponential with boundaries",
h: Histogram{Aggregation: HistogramAggregationExponential, Boundaries: []float64{1.0, 2.0}},
wantErr: "bucket_boundaries must not be set when aggregation is \"exponential\"",
},
{
name: "max_size on non-exponential",
h: Histogram{MaxSize: 5},
wantErr: "max_size is only valid when aggregation is \"exponential\"",
},
{
name: "max_scale on non-exponential",
h: Histogram{MaxScale: 5},
wantErr: "max_scale is only valid when aggregation is \"exponential\"",
},
{
name: "negative max_size",
h: Histogram{Aggregation: HistogramAggregationExponential, MaxSize: -1},
wantErr: "max_size must be a positive integer",
},
{
name: "negative max_scale",
h: Histogram{Aggregation: HistogramAggregationExponential, MaxScale: -1},
wantErr: "max_scale must be between 0 and 20",
},
{
name: "max_scale above 20",
h: Histogram{Aggregation: HistogramAggregationExponential, MaxScale: 21},
wantErr: "max_scale must be between 0 and 20",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.h.Validate()
if tt.wantErr != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}

func TestMetricData(t *testing.T) {
for _, arg := range []struct {
metricData MetricData
Expand All @@ -149,6 +216,7 @@ func TestMetricData(t *testing.T) {
{&Sum{MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeInt}, Async: true}, "Sum", true, true, "Int64ObservableUpDownCounter", true},
{&Sum{MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeDouble}, Async: true}, "Sum", true, true, "Float64ObservableUpDownCounter", true},
{&Histogram{}, "Histogram", true, false, "Histogram", false},
{&Histogram{Aggregation: HistogramAggregationExponential}, "ExponentialHistogram", true, false, "Histogram", false},
} {
assert.Equal(t, arg.wantType, arg.metricData.Type())
assert.Equal(t, arg.wantHasAggregated, arg.metricData.HasAggregated())
Expand Down
8 changes: 8 additions & 0 deletions cmd/mdatagen/internal/samplereceiver/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ Duration of request
| ---- | ----------- | ---------- | --------- |
| s | Histogram | Double | Alpha |

### otelcol_request_duration_exponential

Duration of request (exponential histogram)

| Unit | Metric Type | Value Type | Stability |
| ---- | ----------- | ---------- | --------- |
| s | ExponentialHistogram | Double | Alpha |

## Feature Gates

This component has the following feature gates:
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading