From d91fe239ebb53ea7c657a86d4274c1c2e7f6edf6 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Fri, 6 Feb 2026 16:38:30 +0000 Subject: [PATCH 01/18] Update mdatagen to use metrics from semconv directly This allows metrics defined in the metadata file to directly use the defined semantic convention for that metric. --- cmd/mdatagen/internal/command.go | 28 +++++++++++++++++++ cmd/mdatagen/internal/loader_test.go | 28 +++++++++++++++++-- cmd/mdatagen/internal/metadata.go | 24 ++++++++++++++++ cmd/mdatagen/internal/metric.go | 17 +++++++++++ .../internal/templates/metrics.go.tmpl | 28 +++++++++++++++---- .../internal/templates/metrics_test.go.tmpl | 17 ++++++++++- cmd/mdatagen/metadata-schema.yaml | 3 ++ 7 files changed, 136 insertions(+), 9 deletions(-) diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go index 430cc349c48e..4f04993ee0da 100644 --- a/cmd/mdatagen/internal/command.go +++ b/cmd/mdatagen/internal/command.go @@ -36,6 +36,34 @@ var nonComponents = []string{ "provider", } +type SemConvImport struct { + Package string + Alias string +} + +func (md Metadata) SemConvImports() []SemConvImport { + imports := make(map[string]SemConvImport) + + for _, m := range md.Metrics { + if m.SemanticConvention != nil && m.SemanticConvention.Package != "" { + pkg := m.SemanticConvention.Package + imports[pkg] = SemConvImport{ + Package: pkg, + Alias: pkg, + } + } + } + + result := make([]SemConvImport, 0, len(imports)) + for _, imp := range imports { + result = append(result, imp) + } + sort.Slice(result, func(i, j int) bool { + return result[i].Package < result[j].Package + }) + return result +} + func getVersion() (string, error) { // the second returned value is a boolean, which is true if the binaries are built with module support. info, ok := debug.ReadBuildInfo() diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go index 84c223a12cb4..4a5e3a6d5787 100644 --- a/cmd/mdatagen/internal/loader_test.go +++ b/cmd/mdatagen/internal/loader_test.go @@ -302,9 +302,13 @@ func TestLoadMetadata(t *testing.T) { }, "system.cpu.time": { Signal: Signal{ - Enabled: true, - Stability: Stability{Level: component.StabilityLevelBeta}, - SemanticConvention: &SemanticConvention{SemanticConventionRef: "https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime"}, + Enabled: true, + Stability: Stability{Level: component.StabilityLevelBeta}, + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime", + Package: "systemconv", + Type: "CPUTime", + }, Description: "Monotonic cumulative sum int metric enabled by default.", ExtendedDocumentation: "The metric will be become optional soon.", }, @@ -315,6 +319,24 @@ func TestLoadMetadata(t *testing.T) { Mono: Mono{Monotonic: true}, }, }, + "system.memory.limit": { + Signal: Signal{ + Enabled: true, + Stability: Stability{Level: component.StabilityLevelDevelopment}, + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemmemorylimit", + Package: "systemconv", + Type: "MemoryLimit", + }, + Description: "Total bytes of memory available.", + }, + Unit: strPtr("By"), + Sum: &Sum{ + MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeInt}, + AggregationTemporality: AggregationTemporality{Aggregation: pmetric.AggregationTemporalityCumulative}, + Mono: Mono{Monotonic: false}, + }, + }, "optional.metric": { Signal: Signal{ Enabled: false, diff --git a/cmd/mdatagen/internal/metadata.go b/cmd/mdatagen/internal/metadata.go index 2fa2d3eb5694..c1f6319580a9 100644 --- a/cmd/mdatagen/internal/metadata.go +++ b/cmd/mdatagen/internal/metadata.go @@ -470,6 +470,9 @@ func (mvt ValueType) Primitive() string { type SemanticConvention struct { SemanticConventionRef string `mapstructure:"ref"` + Package string `mapstructure:"package"` + Type string `mapstructure:"type"` + UseSemconvValues *bool `mapstructure:"use_semconv_values"` } type Warnings struct { @@ -641,6 +644,27 @@ func (s Signal) HasConditionalAttributes(attrs map[AttributeName]Attribute) bool return false } +func (s *SemanticConvention) HasSemConvType() bool { + return s != nil && s.Package != "" && s.Type != "" +} + +func (s *SemanticConvention) ShouldUseSemConvValues() bool { + if s == nil || s.Type == "" { + return false + } + if s.UseSemconvValues == nil { + return true + } + return *s.UseSemconvValues +} + +func (s *SemanticConvention) ImportPath(semConvVersion string) string { + if s.Package == "" { + return "" + } + return fmt.Sprintf("go.opentelemetry.io/otel/semconv/%v/%s", semConvVersion, s.Package) +} + type Entity struct { // Type is the type of the entity. Type string `mapstructure:"type"` diff --git a/cmd/mdatagen/internal/metric.go b/cmd/mdatagen/internal/metric.go index 21f97301f190..c94faf016504 100644 --- a/cmd/mdatagen/internal/metric.go +++ b/cmd/mdatagen/internal/metric.go @@ -131,6 +131,11 @@ func (m *Metric) validate(metricName MetricName, semConvVersion string) error { errs = errors.Join(errs, err) } } + if m.SemanticConvention != nil && m.SemanticConvention.HasSemConvType() { + if err := m.validateSemConvType(metricName, semConvVersion); err != nil { + errs = errors.Join(errs, err) + } + } return errs } @@ -169,6 +174,18 @@ func validateSemConvMetricURL(rawURL, semConvVersion, metricName string) error { return nil } +func (m *Metric) validateSemConvType(metricName MetricName, semConvVersion string) error { + sc := m.SemanticConvention + + knownPackages := map[string]bool{ + "systemconv": true, + } + if !knownPackages[sc.Package] { + return fmt.Errorf("unknown semconv package: %s", sc.Package) + } + return nil +} + func (m *Metric) Unmarshal(parser *confmap.Conf) error { if !parser.IsSet("enabled") { return errors.New("missing required field: `enabled`") diff --git a/cmd/mdatagen/internal/templates/metrics.go.tmpl b/cmd/mdatagen/internal/templates/metrics.go.tmpl index 12b81dbb72a5..1d5545aa5b66 100644 --- a/cmd/mdatagen/internal/templates/metrics.go.tmpl +++ b/cmd/mdatagen/internal/templates/metrics.go.tmpl @@ -23,6 +23,9 @@ import ( {{- if .SemConvVersion }} conventions "go.opentelemetry.io/otel/semconv/v{{ .SemConvVersion }}" {{- end }} + {{- range .SemConvImports }} + {{ .Alias }} "go.opentelemetry.io/otel/semconv/v{{ $.SemConvVersion }}/{{ .Package }}" + {{- end }} {{ if .ResourceAttributes -}} "go.opentelemetry.io/collector/filter" {{- end }} @@ -71,11 +74,15 @@ var MapAttribute{{ $name.Render }} = map[string]Attribute{{ $name.Render }}{ {{- end }} var MetricsInfo = metricsInfo{ - {{- range $name, $metric := .Metrics }} - {{ $name.Render }}: metricInfo{ - Name: "{{ $name }}", - }, - {{- end }} + {{- range $name, $metric := .Metrics }} + {{ $name.Render }}: metricInfo{ + {{- if $metric.SemanticConvention.HasSemConvType }} + Name: metricSemConv{{ $name.Render }}.Name(), + {{- else }} + Name: "{{ $name }}", + {{- end }} + }, + {{- end }} } type metricsInfo struct { @@ -118,11 +125,22 @@ type metric{{ $name.Render }} struct { {{- end }} } +{{- if $metric.SemanticConvention.HasSemConvType }} +// metricSemConv{{ $name.Render }} provides access to the semantic convention type +var metricSemConv{{ $name.Render }} = {{ $metric.SemanticConvention.Package }}.{{ $metric.SemanticConvention.Type }}{} +{{- end }} + // init fills {{ $name }} metric with initial data. func (m *metric{{ $name.Render }}) init() { + {{- if $metric.SemanticConvention.ShouldUseSemConvValues }} + m.data.SetName(metricSemConv{{ $name.Render }}.Name()) + m.data.SetDescription(metricSemConv{{ $name.Render }}.Description()) + m.data.SetUnit(metricSemConv{{ $name.Render }}.Unit()) + {{- else }} m.data.SetName("{{ $name }}") m.data.SetDescription("{{ $metric.Description }}") m.data.SetUnit("{{ $metric.Unit }}") + {{- end }} m.data.SetEmpty{{ $metric.Data.Type }}() {{- if $metric.Data.HasMonotonic }} m.data.{{ $metric.Data.Type }}().SetIsMonotonic({{ $metric.Data.Monotonic }}) diff --git a/cmd/mdatagen/internal/templates/metrics_test.go.tmpl b/cmd/mdatagen/internal/templates/metrics_test.go.tmpl index 08be437a3cbf..7568a110c899 100644 --- a/cmd/mdatagen/internal/templates/metrics_test.go.tmpl +++ b/cmd/mdatagen/internal/templates/metrics_test.go.tmpl @@ -15,6 +15,9 @@ import ( {{- end }} "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" + {{- range .SemConvImports }} + {{ .Alias }} "go.opentelemetry.io/otel/semconv/v{{ $.SemConvVersion }}/{{ .Package }}" + {{- end }} ) type testDataSet int @@ -218,7 +221,11 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["{{ $name }}"] = true assert.Equal(t, pmetric.MetricType{{ $metric.Data.Type }}, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).{{ $metric.Data.Type }}().DataPoints().Len()) + {{- if $metric.SemanticConvention.ShouldUseSemConvValues }} + assert.Equal(t, metricSemConv{{ $name.Render }}.Description(), ms.At(i).Description()) + {{- else }} assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) + {{- end }} {{- if len $metric.Unit}} assert.Equal(t, "{{ $metric.Unit }}", ms.At(i).Unit()) {{- else }} @@ -258,7 +265,11 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["{{ $name }}"] = true assert.Equal(t, pmetric.MetricType{{ $metric.Data.Type }}, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).{{ $metric.Data.Type }}().DataPoints().Len()) + {{- if $metric.SemanticConvention.ShouldUseSemConvValues }} + assert.Equal(t, metricSemConv{{ $name.Render }}.Description(), ms.At(i).Description()) + {{- else }} assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) + {{- end }} {{- if len $metric.Unit}} assert.Equal(t, "{{ $metric.Unit }}", ms.At(i).Unit()) {{- else }} @@ -316,7 +327,11 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["{{ $name }}"] = true assert.Equal(t, pmetric.MetricType{{ $metric.Data.Type }}, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).{{ $metric.Data.Type }}().DataPoints().Len()) - assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) + {{- if $metric.SemanticConvention.ShouldUseSemConvValues }} + assert.Equal(t, metricSemConv{{ $name.Render }}.Description(), ms.At(i).Description()) + {{- else }} + assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) + {{- end }} {{- if len $metric.Unit}} assert.Equal(t, "{{ $metric.Unit }}", ms.At(i).Unit()) {{- else }} diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index f6d9142a46ab..52836219caeb 100644 --- a/cmd/mdatagen/metadata-schema.yaml +++ b/cmd/mdatagen/metadata-schema.yaml @@ -160,6 +160,9 @@ metrics: # Optional: the reference to a semantic convention semantic_convention: ref: + package: + type: + use_semconv_values: # Optional: map of event names with the key being the event name and value # being described below. From cae4263521be5cad776fa0edc7405ef60a2b5c8d Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Fri, 6 Feb 2026 16:43:39 +0000 Subject: [PATCH 02/18] Generate code for mdatagen samplereceiver to test linking to semconv --- .../internal/samplereceiver/documentation.md | 8 ++ .../internal/metadata/generated_config.go | 9 ++ .../metadata/generated_config_test.go | 10 ++ .../internal/metadata/generated_metrics.go | 113 +++++++++++++++++- .../metadata/generated_metrics_test.go | 51 +++++++- .../internal/metadata/testdata/config.yaml | 9 ++ .../internal/samplereceiver/metadata.yaml | 17 +++ 7 files changed, 211 insertions(+), 6 deletions(-) diff --git a/cmd/mdatagen/internal/samplereceiver/documentation.md b/cmd/mdatagen/internal/samplereceiver/documentation.md index 363d558a0b16..2bfb8d7a6722 100644 --- a/cmd/mdatagen/internal/samplereceiver/documentation.md +++ b/cmd/mdatagen/internal/samplereceiver/documentation.md @@ -108,6 +108,14 @@ The metric will be become optional soon. | ---- | ----------- | ---------- | ----------------------- | --------- | --------- | ------------------- | | s | Sum | Int | Cumulative | true | Beta | [system.cpu.time](https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime) | +### system.memory.limit + +Total bytes of memory available. + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | Stability | Semantic Convention | +| ---- | ----------- | ---------- | ----------------------- | --------- | --------- | ------------------- | +| By | Sum | Int | Cumulative | false | Development | [system.memory.limit](https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemmemorylimit) | + ## Optional Metrics The following metrics are not emitted by default. Each of them can be enabled by applying the following configuration: diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go index 394caca2b59c..4b9afaa6c7aa 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go @@ -68,6 +68,7 @@ type MetricsConfig struct { ReaggregateMetric MetricConfig `mapstructure:"reaggregate.metric"` ReaggregateMetricWithRequired MetricConfig `mapstructure:"reaggregate.metric.with_required"` SystemCPUTime MetricConfig `mapstructure:"system.cpu.time"` + SystemMemoryLimit MetricConfig `mapstructure:"system.memory.limit"` } func DefaultMetricsConfig() MetricsConfig { @@ -131,6 +132,14 @@ func DefaultMetricsConfig() MetricsConfig { SystemCPUTime: MetricConfig{ Enabled: true, + AggregationStrategy: AggregationStrategySum, + requiredAttributes: []string{}, + definedAttributes: []string{}, + EnabledAttributes: []string{}, + }, + SystemMemoryLimit: MetricConfig{ + Enabled: true, + AggregationStrategy: AggregationStrategySum, requiredAttributes: []string{}, definedAttributes: []string{}, diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go index dd90f1d08603..09e58c79ffb1 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go @@ -67,6 +67,11 @@ func TestMetricsBuilderConfig(t *testing.T) { AggregationStrategy: AggregationStrategySum, EnabledAttributes: []string{}, }, + SystemMemoryLimit: MetricConfig{ + Enabled: true, + AggregationStrategy: AggregationStrategySum, + EnabledAttributes: []string{}, + }, }, ResourceAttributes: ResourceAttributesConfig{ MapResourceAttr: ResourceAttributeConfig{Enabled: true}, @@ -124,6 +129,11 @@ func TestMetricsBuilderConfig(t *testing.T) { AggregationStrategy: AggregationStrategySum, EnabledAttributes: []string{}, }, + SystemMemoryLimit: MetricConfig{ + Enabled: false, + AggregationStrategy: AggregationStrategySum, + EnabledAttributes: []string{}, + }, }, ResourceAttributes: ResourceAttributesConfig{ MapResourceAttr: ResourceAttributeConfig{Enabled: false}, diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go index 71dbabba1e09..827f12efd4e4 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go @@ -9,6 +9,7 @@ import ( "time" conventions "go.opentelemetry.io/otel/semconv/v1.38.0" + systemconv "go.opentelemetry.io/otel/semconv/v1.38.0/systemconv" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/filter" @@ -77,7 +78,10 @@ var MetricsInfo = metricsInfo{ Name: "reaggregate.metric.with_required", }, SystemCPUTime: metricInfo{ - Name: "system.cpu.time", + Name: metricSemConvSystemCPUTime.Name(), + }, + SystemMemoryLimit: metricInfo{ + Name: metricSemConvSystemMemoryLimit.Name(), }, } @@ -90,6 +94,7 @@ type metricsInfo struct { ReaggregateMetric metricInfo ReaggregateMetricWithRequired metricInfo SystemCPUTime metricInfo + SystemMemoryLimit metricInfo } type metricInfo struct { @@ -794,11 +799,14 @@ type metricSystemCPUTime struct { aggDataPoints []int64 // slice containing number of aggregated datapoints at each index } +// metricSemConvSystemCPUTime provides access to the semantic convention type +var metricSemConvSystemCPUTime = systemconv.CPUTime{} + // init fills system.cpu.time metric with initial data. func (m *metricSystemCPUTime) init() { - m.data.SetName("system.cpu.time") - m.data.SetDescription("Monotonic cumulative sum int metric enabled by default.") - m.data.SetUnit("s") + m.data.SetName(metricSemConvSystemCPUTime.Name()) + m.data.SetDescription(metricSemConvSystemCPUTime.Description()) + m.data.SetUnit(metricSemConvSystemCPUTime.Unit()) m.data.SetEmptySum() m.data.Sum().SetIsMonotonic(true) m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -873,6 +881,95 @@ func newMetricSystemCPUTime(cfg MetricConfig) metricSystemCPUTime { return m } +type metricSystemMemoryLimit struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. + aggDataPoints []int64 // slice containing number of aggregated datapoints at each index +} + +// metricSemConvSystemMemoryLimit provides access to the semantic convention type +var metricSemConvSystemMemoryLimit = systemconv.MemoryLimit{} + +// init fills system.memory.limit metric with initial data. +func (m *metricSystemMemoryLimit) init() { + m.data.SetName(metricSemConvSystemMemoryLimit.Name()) + m.data.SetDescription(metricSemConvSystemMemoryLimit.Description()) + m.data.SetUnit(metricSemConvSystemMemoryLimit.Unit()) + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(false) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) +} + +func (m *metricSystemMemoryLimit) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + + dp := pmetric.NewNumberDataPoint() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + + var s string + dps := m.data.Sum().DataPoints() + for i := 0; i < dps.Len(); i++ { + dpi := dps.At(i) + if dp.Attributes().Equal(dpi.Attributes()) && dp.StartTimestamp() == dpi.StartTimestamp() && dp.Timestamp() == dpi.Timestamp() { + switch s = m.config.AggregationStrategy; s { + case AggregationStrategySum, AggregationStrategyAvg: + dpi.SetIntValue(dpi.IntValue() + val) + m.aggDataPoints[i] += 1 + return + case AggregationStrategyMin: + if dpi.IntValue() > val { + dpi.SetIntValue(val) + } + return + case AggregationStrategyMax: + if dpi.IntValue() < val { + dpi.SetIntValue(val) + } + return + } + } + } + + dp.SetIntValue(val) + m.aggDataPoints = append(m.aggDataPoints, 1) + dp.MoveTo(dps.AppendEmpty()) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemMemoryLimit) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemMemoryLimit) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + if m.config.AggregationStrategy == AggregationStrategyAvg { + for i, aggCount := range m.aggDataPoints { + m.data.Sum().DataPoints().At(i).SetIntValue(m.data.Sum().DataPoints().At(i).IntValue() / aggCount) + } + } + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemMemoryLimit(cfg MetricConfig) metricSystemMemoryLimit { + m := metricSystemMemoryLimit{config: cfg} + + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + // MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations // required to produce metric representation defined in metadata and user config. type MetricsBuilder struct { @@ -891,6 +988,7 @@ type MetricsBuilder struct { metricReaggregateMetric metricReaggregateMetric metricReaggregateMetricWithRequired metricReaggregateMetricWithRequired metricSystemCPUTime metricSystemCPUTime + metricSystemMemoryLimit metricSystemMemoryLimit } // MetricBuilderOption applies changes to default metrics builder. @@ -945,6 +1043,7 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, opt metricReaggregateMetric: newMetricReaggregateMetric(mbc.Metrics.ReaggregateMetric), metricReaggregateMetricWithRequired: newMetricReaggregateMetricWithRequired(mbc.Metrics.ReaggregateMetricWithRequired), metricSystemCPUTime: newMetricSystemCPUTime(mbc.Metrics.SystemCPUTime), + metricSystemMemoryLimit: newMetricSystemMemoryLimit(mbc.Metrics.SystemMemoryLimit), resourceAttributeIncludeFilter: make(map[string]filter.Filter), resourceAttributeExcludeFilter: make(map[string]filter.Filter), } @@ -1074,6 +1173,7 @@ func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { mb.metricReaggregateMetric.emit(ils.Metrics()) mb.metricReaggregateMetricWithRequired.emit(ils.Metrics()) mb.metricSystemCPUTime.emit(ils.Metrics()) + mb.metricSystemMemoryLimit.emit(ils.Metrics()) for _, op := range options { op.apply(rm) @@ -1150,6 +1250,11 @@ func (mb *MetricsBuilder) RecordSystemCPUTimeDataPoint(ts pcommon.Timestamp, val mb.metricSystemCPUTime.recordDataPoint(mb.startTime, ts, val) } +// RecordSystemMemoryLimitDataPoint adds a data point to system.memory.limit metric. +func (mb *MetricsBuilder) RecordSystemMemoryLimitDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricSystemMemoryLimit.recordDataPoint(mb.startTime, ts, val) +} + // Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, // and metrics builder should update its startTime and reset it's internal state accordingly. func (mb *MetricsBuilder) Reset(options ...MetricBuilderOption) { diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go index cc852b3623b1..f18ae33582bb 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go @@ -76,6 +76,7 @@ func TestMetricsBuilder(t *testing.T) { aggMap["ReaggregateMetric"] = mb.metricReaggregateMetric.config.AggregationStrategy aggMap["ReaggregateMetricWithRequired"] = mb.metricReaggregateMetricWithRequired.config.AggregationStrategy aggMap["SystemCPUTime"] = mb.metricSystemCPUTime.config.AggregationStrategy + aggMap["SystemMemoryLimit"] = mb.metricSystemMemoryLimit.config.AggregationStrategy expectedWarnings := 0 if tt.metricsSet == testDataSetDefault { @@ -167,6 +168,13 @@ func TestMetricsBuilder(t *testing.T) { mb.RecordSystemCPUTimeDataPoint(ts, 3) } + defaultMetricsCount++ + allMetricsCount++ + mb.RecordSystemMemoryLimitDataPoint(ts, 1) + if tt.name == "reaggregate_set" { + mb.RecordSystemMemoryLimitDataPoint(ts, 3) + } + rb := mb.NewResourceBuilder() rb.SetMapResourceAttr(map[string]any{"key1": "map.resource.attr-val1", "key2": "map.resource.attr-val2"}) rb.SetOptionalResourceAttr("optional.resource.attr-val") @@ -572,7 +580,7 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["system.cpu.time"] = true assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) - assert.Equal(t, "Monotonic cumulative sum int metric enabled by default.", ms.At(i).Description()) + assert.Equal(t, metricSemConvSystemCPUTime.Description(), ms.At(i).Description()) assert.Equal(t, "s", ms.At(i).Unit()) assert.True(t, ms.At(i).Sum().IsMonotonic()) assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) @@ -586,7 +594,7 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["system.cpu.time"] = true assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) - assert.Equal(t, "Monotonic cumulative sum int metric enabled by default.", ms.At(i).Description()) + assert.Equal(t, metricSemConvSystemCPUTime.Description(), ms.At(i).Description()) assert.Equal(t, "s", ms.At(i).Unit()) assert.True(t, ms.At(i).Sum().IsMonotonic()) assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) @@ -605,6 +613,45 @@ func TestMetricsBuilder(t *testing.T) { assert.Equal(t, int64(3), dp.IntValue()) } } + case "system.memory.limit": + if tt.name != "reaggregate_set" { + assert.False(t, validatedMetrics["system.memory.limit"], "Found a duplicate in the metrics slice: system.memory.limit") + validatedMetrics["system.memory.limit"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, metricSemConvSystemMemoryLimit.Description(), ms.At(i).Description()) + assert.Equal(t, "By", ms.At(i).Unit()) + assert.False(t, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + } else { + assert.False(t, validatedMetrics["system.memory.limit"], "Found a duplicate in the metrics slice: system.memory.limit") + validatedMetrics["system.memory.limit"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, metricSemConvSystemMemoryLimit.Description(), ms.At(i).Description()) + assert.Equal(t, "By", ms.At(i).Unit()) + assert.False(t, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + switch aggMap["system.memory.limit"] { + case "sum": + assert.Equal(t, int64(4), dp.IntValue()) + case "avg": + assert.Equal(t, int64(2), dp.IntValue()) + case "min": + assert.Equal(t, int64(1), dp.IntValue()) + case "max": + assert.Equal(t, int64(3), dp.IntValue()) + } + } } } }) diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml b/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml index 9d11a3a39cd8..cb633926cf0c 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml @@ -25,6 +25,9 @@ all_set: system.cpu.time: enabled: true attributes: [] + system.memory.limit: + enabled: true + attributes: [] events: default.event: enabled: true @@ -75,6 +78,9 @@ reaggregate_set: system.cpu.time: enabled: true attributes: [] + system.memory.limit: + enabled: true + attributes: [] events: default.event: enabled: true @@ -125,6 +131,9 @@ none_set: system.cpu.time: enabled: false attributes: [] + system.memory.limit: + enabled: false + attributes: [] events: default.event: enabled: false diff --git a/cmd/mdatagen/internal/samplereceiver/metadata.yaml b/cmd/mdatagen/internal/samplereceiver/metadata.yaml index a2be9e8d02a3..dca54978bd99 100644 --- a/cmd/mdatagen/internal/samplereceiver/metadata.yaml +++ b/cmd/mdatagen/internal/samplereceiver/metadata.yaml @@ -294,6 +294,23 @@ metrics: aggregation_temporality: cumulative semantic_convention: ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime + package: systemconv + type: CPUTime + + system.memory.limit: + enabled: true + stability: + level: development + description: Total bytes of memory available. + unit: By + sum: + value_type: int + monotonic: false + aggregation_temporality: cumulative + semantic_convention: + ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemmemorylimit + package: systemconv + type: MemoryLimit telemetry: metrics: From bd26e92cd5a085834b499681cf38d84631b754f6 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Tue, 10 Feb 2026 16:40:04 +0000 Subject: [PATCH 03/18] Add attribute to the samplereceiver and generated code This is to test mdatagen to generate the correct attribute for semconv --- cmd/mdatagen/internal/loader_test.go | 22 ++++++- .../internal/samplereceiver/documentation.md | 7 +++ .../internal/metadata/generated_config.go | 4 +- .../metadata/generated_config_test.go | 4 +- .../internal/metadata/generated_metrics.go | 63 ++++++++++++++++++- .../metadata/generated_metrics_test.go | 14 ++++- .../internal/metadata/testdata/config.yaml | 4 +- 7 files changed, 105 insertions(+), 13 deletions(-) diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go index 4a5e3a6d5787..cf3df0507702 100644 --- a/cmd/mdatagen/internal/loader_test.go +++ b/cmd/mdatagen/internal/loader_test.go @@ -252,6 +252,23 @@ func TestLoadMetadata(t *testing.T) { FullName: "required_string_attr", RequirementLevel: AttributeRequirementLevelRequired, }, + "cpu": { + Description: "Logical CPU number starting at 0.", + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "cpu", + RequirementLevel: AttributeRequirementLevelRecommended, + }, + "state": { + Description: "Breakdown of CPU usage by type.", + Enum: []string{"idle", "interrupt", "nice", "softirq", "steal", "system", "user", "wait"}, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "state", + RequirementLevel: AttributeRequirementLevelRecommended, + }, }, Metrics: map[MetricName]Metric{ "default.metric": { @@ -302,8 +319,9 @@ func TestLoadMetadata(t *testing.T) { }, "system.cpu.time": { Signal: Signal{ - Enabled: true, - Stability: Stability{Level: component.StabilityLevelBeta}, + Attributes: []AttributeName{"cpu", "state"}, + Enabled: true, + Stability: Stability{Level: component.StabilityLevelBeta}, SemanticConvention: &SemanticConvention{ SemanticConventionRef: "https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime", Package: "systemconv", diff --git a/cmd/mdatagen/internal/samplereceiver/documentation.md b/cmd/mdatagen/internal/samplereceiver/documentation.md index 2bfb8d7a6722..13b9d86b0cda 100644 --- a/cmd/mdatagen/internal/samplereceiver/documentation.md +++ b/cmd/mdatagen/internal/samplereceiver/documentation.md @@ -108,6 +108,13 @@ The metric will be become optional soon. | ---- | ----------- | ---------- | ----------------------- | --------- | --------- | ------------------- | | s | Sum | Int | Cumulative | true | Beta | [system.cpu.time](https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime) | +#### Attributes + +| Name | Description | Values | Requirement Level | +| ---- | ----------- | ------ | -------- | +| cpu | Logical CPU number starting at 0. | Any Str | Recommended | +| state | Breakdown of CPU usage by type. | Str: ``idle``, ``interrupt``, ``nice``, ``softirq``, ``steal``, ``system``, ``user``, ``wait`` | Recommended | + ### system.memory.limit Total bytes of memory available. diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go index 4b9afaa6c7aa..2eff0f516193 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config.go @@ -134,8 +134,8 @@ func DefaultMetricsConfig() MetricsConfig { AggregationStrategy: AggregationStrategySum, requiredAttributes: []string{}, - definedAttributes: []string{}, - EnabledAttributes: []string{}, + definedAttributes: []string{"cpu", "state"}, + EnabledAttributes: []string{"cpu", "state"}, }, SystemMemoryLimit: MetricConfig{ Enabled: true, diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go index 09e58c79ffb1..1c45227c12c7 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_config_test.go @@ -65,7 +65,7 @@ func TestMetricsBuilderConfig(t *testing.T) { SystemCPUTime: MetricConfig{ Enabled: true, AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []string{}, + EnabledAttributes: []string{"cpu", "state"}, }, SystemMemoryLimit: MetricConfig{ Enabled: true, @@ -127,7 +127,7 @@ func TestMetricsBuilderConfig(t *testing.T) { SystemCPUTime: MetricConfig{ Enabled: false, AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []string{}, + EnabledAttributes: []string{"cpu", "state"}, }, SystemMemoryLimit: MetricConfig{ Enabled: false, diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go index 827f12efd4e4..c0a3ffd094de 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics.go @@ -55,6 +55,56 @@ var MapAttributeEnumAttr = map[string]AttributeEnumAttr{ "blue": AttributeEnumAttrBlue, } +// AttributeState specifies the value state attribute. +type AttributeState int + +const ( + _ AttributeState = iota + AttributeStateIdle + AttributeStateInterrupt + AttributeStateNice + AttributeStateSoftirq + AttributeStateSteal + AttributeStateSystem + AttributeStateUser + AttributeStateWait +) + +// String returns the string representation of the AttributeState. +func (av AttributeState) String() string { + switch av { + case AttributeStateIdle: + return "idle" + case AttributeStateInterrupt: + return "interrupt" + case AttributeStateNice: + return "nice" + case AttributeStateSoftirq: + return "softirq" + case AttributeStateSteal: + return "steal" + case AttributeStateSystem: + return "system" + case AttributeStateUser: + return "user" + case AttributeStateWait: + return "wait" + } + return "" +} + +// MapAttributeState is a helper map of string to AttributeState attribute value. +var MapAttributeState = map[string]AttributeState{ + "idle": AttributeStateIdle, + "interrupt": AttributeStateInterrupt, + "nice": AttributeStateNice, + "softirq": AttributeStateSoftirq, + "steal": AttributeStateSteal, + "system": AttributeStateSystem, + "user": AttributeStateUser, + "wait": AttributeStateWait, +} + var MetricsInfo = metricsInfo{ DefaultMetric: metricInfo{ Name: "default.metric", @@ -810,9 +860,10 @@ func (m *metricSystemCPUTime) init() { m.data.SetEmptySum() m.data.Sum().SetIsMonotonic(true) m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + m.data.Sum().DataPoints().EnsureCapacity(m.capacity) } -func (m *metricSystemCPUTime) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { +func (m *metricSystemCPUTime) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, cpuAttributeValue string, stateAttributeValue string) { if !m.config.Enabled { return } @@ -820,6 +871,12 @@ func (m *metricSystemCPUTime) recordDataPoint(start pcommon.Timestamp, ts pcommo dp := pmetric.NewNumberDataPoint() dp.SetStartTimestamp(start) dp.SetTimestamp(ts) + if slices.Contains(m.config.EnabledAttributes, "cpu") { + dp.Attributes().PutStr("cpu", cpuAttributeValue) + } + if slices.Contains(m.config.EnabledAttributes, "state") { + dp.Attributes().PutStr("state", stateAttributeValue) + } var s string dps := m.data.Sum().DataPoints() @@ -1246,8 +1303,8 @@ func (mb *MetricsBuilder) RecordReaggregateMetricWithRequiredDataPoint(ts pcommo } // RecordSystemCPUTimeDataPoint adds a data point to system.cpu.time metric. -func (mb *MetricsBuilder) RecordSystemCPUTimeDataPoint(ts pcommon.Timestamp, val int64) { - mb.metricSystemCPUTime.recordDataPoint(mb.startTime, ts, val) +func (mb *MetricsBuilder) RecordSystemCPUTimeDataPoint(ts pcommon.Timestamp, val int64, cpuAttributeValue string, stateAttributeValue AttributeState) { + mb.metricSystemCPUTime.recordDataPoint(mb.startTime, ts, val, cpuAttributeValue, stateAttributeValue.String()) } // RecordSystemMemoryLimitDataPoint adds a data point to system.memory.limit metric. diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go index f18ae33582bb..1a3f15b08079 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go @@ -163,9 +163,9 @@ func TestMetricsBuilder(t *testing.T) { defaultMetricsCount++ allMetricsCount++ - mb.RecordSystemCPUTimeDataPoint(ts, 1) + mb.RecordSystemCPUTimeDataPoint(ts, 1, "cpu-val", AttributeStateIdle) if tt.name == "reaggregate_set" { - mb.RecordSystemCPUTimeDataPoint(ts, 3) + mb.RecordSystemCPUTimeDataPoint(ts, 3, "cpu-val-2", AttributeStateInterrupt) } defaultMetricsCount++ @@ -589,6 +589,12 @@ func TestMetricsBuilder(t *testing.T) { assert.Equal(t, ts, dp.Timestamp()) assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) assert.Equal(t, int64(1), dp.IntValue()) + attrVal, ok := dp.Attributes().Get("cpu") + assert.True(t, ok) + assert.Equal(t, "cpu-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("state") + assert.True(t, ok) + assert.Equal(t, "idle", attrVal.Str()) } else { assert.False(t, validatedMetrics["system.cpu.time"], "Found a duplicate in the metrics slice: system.cpu.time") validatedMetrics["system.cpu.time"] = true @@ -612,6 +618,10 @@ func TestMetricsBuilder(t *testing.T) { case "max": assert.Equal(t, int64(3), dp.IntValue()) } + _, ok := dp.Attributes().Get("cpu") + assert.False(t, ok) + _, ok = dp.Attributes().Get("state") + assert.False(t, ok) } case "system.memory.limit": if tt.name != "reaggregate_set" { diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml b/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml index cb633926cf0c..120cdd2e2427 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/testdata/config.yaml @@ -24,7 +24,7 @@ all_set: attributes: ["required_string_attr","string_attr","boolean_attr"] system.cpu.time: enabled: true - attributes: [] + attributes: ["cpu","state"] system.memory.limit: enabled: true attributes: [] @@ -130,7 +130,7 @@ none_set: attributes: ["required_string_attr","string_attr","boolean_attr"] system.cpu.time: enabled: false - attributes: [] + attributes: ["cpu","state"] system.memory.limit: enabled: false attributes: [] From ebdb47afb15100c144d75b5d2a82e92bd24d5c36 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Tue, 10 Feb 2026 16:40:50 +0000 Subject: [PATCH 04/18] Added attributes to metadata for mdatagen testing --- cmd/mdatagen/internal/samplereceiver/metadata.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/mdatagen/internal/samplereceiver/metadata.yaml b/cmd/mdatagen/internal/samplereceiver/metadata.yaml index dca54978bd99..545c1ea1fec3 100644 --- a/cmd/mdatagen/internal/samplereceiver/metadata.yaml +++ b/cmd/mdatagen/internal/samplereceiver/metadata.yaml @@ -104,6 +104,10 @@ attributes: type: string requirement_level: conditionally_required + cpu: + description: Logical CPU number starting at 0. + type: string + enum_attr: description: Attribute with a known set of string values. type: string @@ -132,6 +136,11 @@ attributes: description: Attribute with a slice value. type: slice + state: + description: Breakdown of CPU usage by type. + type: string + enum: [idle, interrupt, nice, softirq, steal, system, user, wait] + string_attr: description: Attribute with any string value. type: string @@ -292,6 +301,7 @@ metrics: value_type: int monotonic: true aggregation_temporality: cumulative + attributes: [cpu, state] semantic_convention: ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime package: systemconv From 7304b5bac58768e3ec11eb59e9cef5a788433be4 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Fri, 13 Feb 2026 12:32:54 +0000 Subject: [PATCH 05/18] Update mdatagen to infer the semconv types from the semconv url When the semantic_convention: ref url is provided mdatagen now infers the pkg and type from that url. This only works for semconv >= v1.32.0 due to the generated go code only containing the semconv types from this version and above. --- cmd/mdatagen/go.mod | 1 + cmd/mdatagen/go.sum | 2 + cmd/mdatagen/internal/loader.go | 2 + cmd/mdatagen/internal/loader_test.go | 5 + cmd/mdatagen/internal/metadata.go | 61 +++++++ cmd/mdatagen/internal/metadata_test.go | 171 ++++++++++++++++++ cmd/mdatagen/internal/metric.go | 22 +-- .../internal/samplereceiver/metadata.yaml | 3 +- ...invalid_metric_semconvref_old_version.yaml | 29 +++ cmd/mdatagen/metadata-schema.yaml | 8 +- 10 files changed, 284 insertions(+), 20 deletions(-) create mode 100644 cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml diff --git a/cmd/mdatagen/go.mod b/cmd/mdatagen/go.mod index 2ab442e8444d..bc567d9a5beb 100644 --- a/cmd/mdatagen/go.mod +++ b/cmd/mdatagen/go.mod @@ -33,6 +33,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.1 go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/mod v0.33.0 golang.org/x/text v0.33.0 ) diff --git a/cmd/mdatagen/go.sum b/cmd/mdatagen/go.sum index c35b187dc22c..6a7f15e755ca 100644 --- a/cmd/mdatagen/go.sum +++ b/cmd/mdatagen/go.sum @@ -86,6 +86,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= diff --git a/cmd/mdatagen/internal/loader.go b/cmd/mdatagen/internal/loader.go index 159c1daada8e..35bc22a4eb3b 100644 --- a/cmd/mdatagen/internal/loader.go +++ b/cmd/mdatagen/internal/loader.go @@ -60,6 +60,8 @@ func LoadMetadata(filePath string) (Metadata, error) { md.GeneratedPackageName = "metadata" } + md.inferSemConvTypes() + if err := md.Validate(); err != nil { return md, err } diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go index cf3df0507702..8d43fd8efa68 100644 --- a/cmd/mdatagen/internal/loader_test.go +++ b/cmd/mdatagen/internal/loader_test.go @@ -650,6 +650,11 @@ func TestLoadMetadata(t *testing.T) { want: Metadata{}, wantErr: "metric \"default.metric\": invalid semantic-conventions URL: want https://github.com/open-telemetry/semantic-conventions/blob/v1.37.2/*#metric-defaultmetric, got \"https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime\"", }, + { + name: "testdata/invalid_metric_semconvref_old_version.yaml", + want: Metadata{}, + wantErr: "metric \"default.metric\": semantic_convention requires sem_conv_version >= 1.32.0", + }, { name: "testdata/no_metric_stability.yaml", want: Metadata{}, diff --git a/cmd/mdatagen/internal/metadata.go b/cmd/mdatagen/internal/metadata.go index c1f6319580a9..3adc66964122 100644 --- a/cmd/mdatagen/internal/metadata.go +++ b/cmd/mdatagen/internal/metadata.go @@ -665,6 +665,67 @@ func (s *SemanticConvention) ImportPath(semConvVersion string) string { return fmt.Sprintf("go.opentelemetry.io/otel/semconv/%v/%s", semConvVersion, s.Package) } +// inferSemConvTypes auto-populates Package and Type on any SemanticConvention +// that has a ref URL but is missing Package/Type, by inferring them from the metric name. +func (md *Metadata) inferSemConvTypes() { + for name, m := range md.Metrics { + sc := m.SemanticConvention + if sc == nil || sc.SemanticConventionRef == "" { + continue + } + if sc.Package == "" && sc.Type == "" { + pkg, typeName, err := InferSemConvFromMetricName(string(name)) + if err != nil { + // If inference fails, leave them empty; validation will catch it. + continue + } + sc.Package = pkg + sc.Type = typeName + } + } +} + +// semconvAcronyms extends the golint acronyms with additional ones +// used by the OTel semconv Go codegen that are not in the standard golint list. +var semconvAcronyms = map[string]bool{ + "IO": true, +} + +// InferSemConvFromMetricName derives the semconv Go package and type name +// from a dotted metric name. For example, "system.cpu.time" yields +// package "systemconv" and type "CPUTime". +func InferSemConvFromMetricName(metricName string) (pkg, typeName string, err error) { + parts := strings.Split(metricName, ".") + if len(parts) < 2 { + return "", "", fmt.Errorf("metric name %q must have at least 2 segments to infer semconv type", metricName) + } + pkg = parts[0] + "conv" + + remaining := strings.Join(parts[1:], ".") + typeName, err = formatSemConvIdentifier(remaining) + return pkg, typeName, err +} + +// formatSemConvIdentifier converts a dotted/underscored identifier into a +// PascalCase Go identifier, applying both the standard golint acronyms and +// the semconv-specific acronyms. +func formatSemConvIdentifier(s string) (string, error) { + ident, err := FormatIdentifier(s, true) + if err != nil { + return "", err + } + + // FormatIdentifier only knows about golint.Acronyms. We need to fix up + // any semconv-specific acronyms that it missed. We do this by scanning + // for Title-cased versions (e.g. "Io") and replacing them with the + // upper-case acronym (e.g. "IO") at word boundaries. + for acronym := range semconvAcronyms { + titleForm := strings.ToUpper(acronym[:1]) + strings.ToLower(acronym[1:]) + ident = strings.ReplaceAll(ident, titleForm, acronym) + } + return ident, nil +} + type Entity struct { // Type is the type of the entity. Type string `mapstructure:"type"` diff --git a/cmd/mdatagen/internal/metadata_test.go b/cmd/mdatagen/internal/metadata_test.go index 49e21eee734b..eb371500bb09 100644 --- a/cmd/mdatagen/internal/metadata_test.go +++ b/cmd/mdatagen/internal/metadata_test.go @@ -518,3 +518,174 @@ func TestValidateFeatureGatesNotSorted(t *testing.T) { require.Error(t, err) assert.ErrorContains(t, err, "feature gates must be sorted by ID") } + +func TestInferSemConvFromMetricName(t *testing.T) { + tests := []struct { + name string + metric string + wantPkg string + wantType string + wantErr bool + }{ + { + name: "go.goroutine.count", + metric: "go.goroutine.count", + wantPkg: "goconv", + wantType: "GoroutineCount", + }, + { + name: "k8s.container.cpu.limit", + metric: "k8s.container.cpu.limit", + wantPkg: "k8sconv", + wantType: "ContainerCPULimit", + }, + { + name: "system.cpu.time", + metric: "system.cpu.time", + wantPkg: "systemconv", + wantType: "CPUTime", + }, + { + name: "system.cpu.logical.count", + metric: "system.cpu.logical.count", + wantPkg: "systemconv", + wantType: "CPULogicalCount", + }, + { + name: "system.memory.limit", + metric: "system.memory.limit", + wantPkg: "systemconv", + wantType: "MemoryLimit", + }, + { + name: "system.disk.io uses IO acronym", + metric: "system.disk.io", + wantPkg: "systemconv", + wantType: "DiskIO", + }, + { + name: "system.disk.io_time handles underscores and IO", + metric: "system.disk.io_time", + wantPkg: "systemconv", + wantType: "DiskIOTime", + }, + { + name: "system.network.io", + metric: "system.network.io", + wantPkg: "systemconv", + wantType: "NetworkIO", + }, + { + name: "system.linux.memory.available", + metric: "system.linux.memory.available", + wantPkg: "systemconv", + wantType: "LinuxMemoryAvailable", + }, + { + name: "system.linux.memory.slab.usage", + metric: "system.linux.memory.slab.usage", + wantPkg: "systemconv", + wantType: "LinuxMemorySlabUsage", + }, + { + name: "system.uptime single remaining segment", + metric: "system.uptime", + wantPkg: "systemconv", + wantType: "Uptime", + }, + { + name: "system.filesystem.utilization", + metric: "system.filesystem.utilization", + wantPkg: "systemconv", + wantType: "FilesystemUtilization", + }, + { + name: "system.paging.faults", + metric: "system.paging.faults", + wantPkg: "systemconv", + wantType: "PagingFaults", + }, + { + name: "system.network.packet.dropped", + metric: "system.network.packet.dropped", + wantPkg: "systemconv", + wantType: "NetworkPacketDropped", + }, + { + name: "http.server.request.duration uses different namespace", + metric: "http.server.request.duration", + wantPkg: "httpconv", + wantType: "ServerRequestDuration", + }, + { + name: "single segment fails", + metric: "system", + wantErr: true, + }, + { + name: "empty string fails", + metric: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkg, typeName, err := InferSemConvFromMetricName(tt.metric) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantPkg, pkg) + assert.Equal(t, tt.wantType, typeName) + }) + } +} + +func TestInferSemConvTypes(t *testing.T) { + md := &Metadata{ + Metrics: map[MetricName]Metric{ + "system.cpu.time": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + }, + }, + }, + "system.disk.io": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + }, + }, + }, + // Explicit values should not be overwritten + "system.memory.limit": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + Package: "custom", + Type: "Custom", + }, + }, + }, + // No semantic convention, should be left alone + "default.metric": { + Signal: Signal{}, + }, + }, + } + + md.inferSemConvTypes() + + assert.Equal(t, "systemconv", md.Metrics["system.cpu.time"].SemanticConvention.Package) + assert.Equal(t, "CPUTime", md.Metrics["system.cpu.time"].SemanticConvention.Type) + assert.Equal(t, "systemconv", md.Metrics["system.disk.io"].SemanticConvention.Package) + assert.Equal(t, "DiskIO", md.Metrics["system.disk.io"].SemanticConvention.Type) + // Explicit values preserved + assert.Equal(t, "custom", md.Metrics["system.memory.limit"].SemanticConvention.Package) + assert.Equal(t, "Custom", md.Metrics["system.memory.limit"].SemanticConvention.Type) + // No semantic convention + assert.Nil(t, md.Metrics["default.metric"].SemanticConvention) +} diff --git a/cmd/mdatagen/internal/metric.go b/cmd/mdatagen/internal/metric.go index c94faf016504..bf680b30dcb4 100644 --- a/cmd/mdatagen/internal/metric.go +++ b/cmd/mdatagen/internal/metric.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" + "golang.org/x/mod/semver" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -126,16 +127,15 @@ func (m *Metric) validate(metricName MetricName, semConvVersion string) error { if m.Gauge != nil { errs = errors.Join(errs, m.Gauge.Validate()) } + if m.SemanticConvention != nil && semver.Compare("v"+semConvVersion, "v1.32.0") < 0 { + errs = errors.Join(errs, fmt.Errorf("semantic_convention requires sem_conv_version >= 1.32.0")) + return errs + } if m.SemanticConvention != nil { if err := validateSemConvMetricURL(m.SemanticConvention.SemanticConventionRef, semConvVersion, string(metricName)); err != nil { errs = errors.Join(errs, err) } } - if m.SemanticConvention != nil && m.SemanticConvention.HasSemConvType() { - if err := m.validateSemConvType(metricName, semConvVersion); err != nil { - errs = errors.Join(errs, err) - } - } return errs } @@ -174,18 +174,6 @@ func validateSemConvMetricURL(rawURL, semConvVersion, metricName string) error { return nil } -func (m *Metric) validateSemConvType(metricName MetricName, semConvVersion string) error { - sc := m.SemanticConvention - - knownPackages := map[string]bool{ - "systemconv": true, - } - if !knownPackages[sc.Package] { - return fmt.Errorf("unknown semconv package: %s", sc.Package) - } - return nil -} - func (m *Metric) Unmarshal(parser *confmap.Conf) error { if !parser.IsSet("enabled") { return errors.New("missing required field: `enabled`") diff --git a/cmd/mdatagen/internal/samplereceiver/metadata.yaml b/cmd/mdatagen/internal/samplereceiver/metadata.yaml index 545c1ea1fec3..1642b9802701 100644 --- a/cmd/mdatagen/internal/samplereceiver/metadata.yaml +++ b/cmd/mdatagen/internal/samplereceiver/metadata.yaml @@ -317,10 +317,9 @@ metrics: value_type: int monotonic: false aggregation_temporality: cumulative + # package and type are intentionally omitted to test auto-inference from metric name semantic_convention: ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemmemorylimit - package: systemconv - type: MemoryLimit telemetry: metrics: diff --git a/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml b/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml new file mode 100644 index 000000000000..15be45ce0686 --- /dev/null +++ b/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml @@ -0,0 +1,29 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +sem_conv_version: 1.9.0 + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + stability: + level: development + semantic_convention: + ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.9.0/docs/system/system-metrics.md#metric-systemcputime + unit: s + sum: + value_type: int + monotonic: true + aggregation_temporality: cumulative + diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index 52836219caeb..37809306d336 100644 --- a/cmd/mdatagen/metadata-schema.yaml +++ b/cmd/mdatagen/metadata-schema.yaml @@ -157,11 +157,17 @@ metrics: since: # Required: migration note note: - # Optional: the reference to a semantic convention + # Optional: the reference to a semantic convention. + # When ref is provided, package and type are auto-inferred from the metric name + # (e.g. "system.cpu.time" → package "systemconv", type "CPUTime"). + # Explicit package/type can be specified to override the inferred values. semantic_convention: ref: + # Optional: semconv Go package name. Inferred from metric name if omitted (e.g. "systemconv"). package: + # Optional: semconv Go type name. Inferred from metric name if omitted (e.g. "CPUTime"). type: + # Optional: whether to use semconv Name/Description/Unit in generated code. Defaults to true. use_semconv_values: # Optional: map of event names with the key being the event name and value From f69e1ab3827c6f11f218444caaa750bd6f4d7cbe Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Fri, 13 Feb 2026 12:34:46 +0000 Subject: [PATCH 06/18] Generate code for samplereceiver to test mdatagen semconv linking --- .../internal/metadata/generated_metrics.go | 12 ++++++++---- .../internal/metadata/generated_metrics_test.go | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics.go b/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics.go index c1dd3c192eec..b21d8fc1b86e 100644 --- a/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics.go +++ b/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics.go @@ -9,6 +9,7 @@ import ( "time" conventions "go.opentelemetry.io/otel/semconv/v1.38.0" + systemconv "go.opentelemetry.io/otel/semconv/v1.38.0/systemconv" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/filter" @@ -74,7 +75,7 @@ var MetricsInfo = metricsInfo{ Name: "reaggregate.metric", }, SystemCPUTime: metricInfo{ - Name: "system.cpu.time", + Name: metricSemConvSystemCPUTime.Name(), }, } @@ -665,11 +666,14 @@ type metricSystemCPUTime struct { aggDataPoints []int64 // slice containing number of aggregated datapoints at each index } +// metricSemConvSystemCPUTime provides access to the semantic convention type +var metricSemConvSystemCPUTime = systemconv.CPUTime{} + // init fills system.cpu.time metric with initial data. func (m *metricSystemCPUTime) init() { - m.data.SetName("system.cpu.time") - m.data.SetDescription("Monotonic cumulative sum int metric enabled by default.") - m.data.SetUnit("s") + m.data.SetName(metricSemConvSystemCPUTime.Name()) + m.data.SetDescription(metricSemConvSystemCPUTime.Description()) + m.data.SetUnit(metricSemConvSystemCPUTime.Unit()) m.data.SetEmptySum() m.data.Sum().SetIsMonotonic(true) m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) diff --git a/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go index f198712cda41..d92b7e56dc69 100644 --- a/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go @@ -503,7 +503,7 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["system.cpu.time"] = true assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) - assert.Equal(t, "Monotonic cumulative sum int metric enabled by default.", ms.At(i).Description()) + assert.Equal(t, metricSemConvSystemCPUTime.Description(), ms.At(i).Description()) assert.Equal(t, "s", ms.At(i).Unit()) assert.True(t, ms.At(i).Sum().IsMonotonic()) assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) @@ -517,7 +517,7 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics["system.cpu.time"] = true assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) - assert.Equal(t, "Monotonic cumulative sum int metric enabled by default.", ms.At(i).Description()) + assert.Equal(t, metricSemConvSystemCPUTime.Description(), ms.At(i).Description()) assert.Equal(t, "s", ms.At(i).Unit()) assert.True(t, ms.At(i).Sum().IsMonotonic()) assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) From 3ecbec6ffd8b1ab2b769021b02925a0e07405e3e Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Fri, 13 Feb 2026 16:39:23 +0000 Subject: [PATCH 07/18] Update semconv version error in mdatagen to use errors.New --- cmd/mdatagen/internal/metric.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/mdatagen/internal/metric.go b/cmd/mdatagen/internal/metric.go index bf680b30dcb4..9bea10602e03 100644 --- a/cmd/mdatagen/internal/metric.go +++ b/cmd/mdatagen/internal/metric.go @@ -128,7 +128,7 @@ func (m *Metric) validate(metricName MetricName, semConvVersion string) error { errs = errors.Join(errs, m.Gauge.Validate()) } if m.SemanticConvention != nil && semver.Compare("v"+semConvVersion, "v1.32.0") < 0 { - errs = errors.Join(errs, fmt.Errorf("semantic_convention requires sem_conv_version >= 1.32.0")) + errs = errors.Join(errs, errors.New("semantic_convention requires sem_conv_version >= 1.32.0")) return errs } if m.SemanticConvention != nil { From f58fd75e99fea12328d11bcfd0254efa48b6b35b Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Mon, 16 Feb 2026 15:36:43 +0000 Subject: [PATCH 08/18] Fix mdatagen after merge with main, stability has moved to not nested --- .../samplereceiver/internal/metadata/generated_metrics_test.go | 1 + cmd/mdatagen/internal/samplereceiver/metadata.yaml | 3 +-- .../testdata/invalid_metric_semconvref_old_version.yaml | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go index 6c82224658ef..100aa89b16f2 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go @@ -195,6 +195,7 @@ func TestMetricsBuilder(t *testing.T) { assert.Empty(t, mb.metricReaggregateMetric.aggDataPoints) assert.Empty(t, mb.metricReaggregateMetricWithRequired.aggDataPoints) assert.Empty(t, mb.metricSystemCPUTime.aggDataPoints) + assert.Empty(t, mb.metricSystemMemoryLimit.aggDataPoints) } if tt.expectEmpty { diff --git a/cmd/mdatagen/internal/samplereceiver/metadata.yaml b/cmd/mdatagen/internal/samplereceiver/metadata.yaml index 8c0aab5c9f24..e8bec9453528 100644 --- a/cmd/mdatagen/internal/samplereceiver/metadata.yaml +++ b/cmd/mdatagen/internal/samplereceiver/metadata.yaml @@ -301,8 +301,7 @@ metrics: system.memory.limit: enabled: true - stability: - level: development + stability: development description: Total bytes of memory available. unit: By sum: diff --git a/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml b/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml index 15be45ce0686..7570243e9ab3 100644 --- a/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml +++ b/cmd/mdatagen/internal/testdata/invalid_metric_semconvref_old_version.yaml @@ -17,8 +17,7 @@ metrics: enabled: true description: Monotonic cumulative sum int metric enabled by default. extended_documentation: The metric will be become optional soon. - stability: - level: development + stability: development semantic_convention: ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.9.0/docs/system/system-metrics.md#metric-systemcputime unit: s From 569dbbaf89abc4e9c32517596e8927da069473ad Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Mon, 16 Feb 2026 16:31:33 +0000 Subject: [PATCH 09/18] Add chlog for linking metrics to semantic conventions for mdatagen --- ...d-attributes-from-metadata-to-semconv.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml diff --git a/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml b/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml new file mode 100644 index 000000000000..6538e5eb6b63 --- /dev/null +++ b/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml @@ -0,0 +1,28 @@ +# 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: Add support for linking metrics to semantic cenventions definitions + +# One or more tracking issues or pull requests related to the change +issues: [13297] + +# (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: | + Metrics in metadata.yaml can now reference semantic convention definitions via `semantic_convention.ref`. + The generated code will use semconv types for metric name, description, and unit. + Package and type are auto-inferred from the metric name (e.g., `system.cpu.time` → `systemconv.CPUTime`). + +# 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: [user] From 137ef5303d8d9840ebb9081d5e8b78858a8ebf63 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Mon, 16 Feb 2026 17:02:23 +0000 Subject: [PATCH 10/18] Move SemConvImports to metadata.go from command.go --- cmd/mdatagen/internal/command.go | 28 ---------------------------- cmd/mdatagen/internal/metadata.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go index 9456685e13f8..63462f281676 100644 --- a/cmd/mdatagen/internal/command.go +++ b/cmd/mdatagen/internal/command.go @@ -36,34 +36,6 @@ var nonComponents = []string{ "provider", } -type SemConvImport struct { - Package string - Alias string -} - -func (md Metadata) SemConvImports() []SemConvImport { - imports := make(map[string]SemConvImport) - - for _, m := range md.Metrics { - if m.SemanticConvention != nil && m.SemanticConvention.Package != "" { - pkg := m.SemanticConvention.Package - imports[pkg] = SemConvImport{ - Package: pkg, - Alias: pkg, - } - } - } - - result := make([]SemConvImport, 0, len(imports)) - for _, imp := range imports { - result = append(result, imp) - } - sort.Slice(result, func(i, j int) bool { - return result[i].Package < result[j].Package - }) - return result -} - func getVersion() (string, error) { // the second returned value is a boolean, which is true if the binaries are built with module support. info, ok := debug.ReadBuildInfo() diff --git a/cmd/mdatagen/internal/metadata.go b/cmd/mdatagen/internal/metadata.go index 04abd7901d42..f8b0827e2e13 100644 --- a/cmd/mdatagen/internal/metadata.go +++ b/cmd/mdatagen/internal/metadata.go @@ -8,6 +8,7 @@ import ( "fmt" "regexp" "slices" + "sort" "strconv" "strings" @@ -809,3 +810,31 @@ type FeatureGate struct { // ReferenceURL is the URL with contextual information about the feature gate. ReferenceURL string `mapstructure:"reference_url"` } + +type SemConvImport struct { + Package string + Alias string +} + +func (md Metadata) SemConvImports() []SemConvImport { + imports := make(map[string]SemConvImport) + + for _, m := range md.Metrics { + if m.SemanticConvention != nil && m.SemanticConvention.Package != "" { + pkg := m.SemanticConvention.Package + imports[pkg] = SemConvImport{ + Package: pkg, + Alias: pkg, + } + } + } + + result := make([]SemConvImport, 0, len(imports)) + for _, imp := range imports { + result = append(result, imp) + } + sort.Slice(result, func(i, j int) bool { + return result[i].Package < result[j].Package + }) + return result +} From f9038c5e0eadef7f65d5a45c7264e6e358cf3b94 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Mon, 16 Feb 2026 17:04:21 +0000 Subject: [PATCH 11/18] Fix spelling mistakes in chlog and metadata-schema --- ...ink-metrics-and-attributes-from-metadata-to-semconv.yaml | 2 +- cmd/mdatagen/metadata-schema.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml b/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml index 6538e5eb6b63..7bf730ef2a01 100644 --- a/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml +++ b/.chloggen/mdatagen-link-metrics-and-attributes-from-metadata-to-semconv.yaml @@ -7,7 +7,7 @@ change_type: enhancement component: cmd/mdatagen # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). -note: Add support for linking metrics to semantic cenventions definitions +note: Add support for linking metrics to semantic conventions definitions # One or more tracking issues or pull requests related to the change issues: [13297] diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index 2aaa03f74608..357bc082c08b 100644 --- a/cmd/mdatagen/metadata-schema.yaml +++ b/cmd/mdatagen/metadata-schema.yaml @@ -169,11 +169,11 @@ metrics: # Explicit package/type can be specified to override the inferred values. semantic_convention: ref: - # Optional: semconv Go package name. Inferred from metric name if omitted (e.g. "systemconv"). + # Optional: semantic convention Go package name. Inferred from metric name if omitted (e.g. "systemconv"). package: - # Optional: semconv Go type name. Inferred from metric name if omitted (e.g. "CPUTime"). + # Optional: semantic convention Go type name. Inferred from metric name if omitted (e.g. "CPUTime"). type: - # Optional: whether to use semconv Name/Description/Unit in generated code. Defaults to true. + # Optional: whether to use semantic convention Name/Description/Unit in generated code. Defaults to true. use_semconv_values: # Optional: map of event names with the key being the event name and value From 4cdaf1997c61788b5d79293da829d9d636de9145 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Tue, 17 Feb 2026 09:19:14 +0000 Subject: [PATCH 12/18] Add systemconv to cspell as this is a valid package name in semconv --- .github/workflows/utils/cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/utils/cspell.json b/.github/workflows/utils/cspell.json index 8db53b22a41b..23fd5f7f1275 100644 --- a/.github/workflows/utils/cspell.json +++ b/.github/workflows/utils/cspell.json @@ -459,6 +459,7 @@ "subpackages", "swiatekm", "syft", + "systemconv", "tailsampling", "tchannel", "telemetrygen", From d3844fb8a5e6bed200b3d44c1bf105236ff1efdb Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Tue, 17 Feb 2026 16:55:51 +0000 Subject: [PATCH 13/18] Add unit tests to cover SemConvImport and ShouldUseSemConvValues --- cmd/mdatagen/internal/metadata_test.go | 123 +++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/cmd/mdatagen/internal/metadata_test.go b/cmd/mdatagen/internal/metadata_test.go index cbd514ff23ec..c782fe58a680 100644 --- a/cmd/mdatagen/internal/metadata_test.go +++ b/cmd/mdatagen/internal/metadata_test.go @@ -705,3 +705,126 @@ func TestInferSemConvTypes(t *testing.T) { // No semantic convention assert.Nil(t, md.Metrics["default.metric"].SemanticConvention) } + +func TestSemConvImports(t *testing.T) { + tests := []struct { + name string + md Metadata + want []SemConvImport + }{ + { + name: "empty semconv import", + md: Metadata{}, + want: []SemConvImport{}, + }, + { + name: "Multiple packages sorted alphabetically", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "system.cpu.time": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + Package: "systemconv", + }, + }, + }, + "http.server.duration": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + Package: "httpconv", + }, + }, + }, + }, + }, + want: []SemConvImport{ + { + Package: "httpconv", + Alias: "httpconv", + }, + { + Package: "systemconv", + Alias: "systemconv", + }, + }, + }, + { + name: "duplicate packages deduplicates", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "system.cpu.time": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + Package: "systemconv", + }, + }, + }, + "system.memory.usage": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + Package: "systemconv", + }, + }, + }, + }, + }, + want: []SemConvImport{ + { + Package: "systemconv", + Alias: "systemconv", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.md.SemConvImports() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestShouldUseSemConvValues(t *testing.T) { + trueValue := true + falseValue := false + + tests := []struct { + name string + sc *SemanticConvention + want bool + }{ + { + name: "nil semantic convention", + sc: nil, + want: false, + }, + { + name: "empty type", + sc: &SemanticConvention{Type: ""}, + want: false, + }, + { + name: "type set, UseSemconvValues nil (default true)", + sc: &SemanticConvention{Type: "CPUTime"}, + want: true, + }, + { + name: "type set, UseSemconvValues true", + sc: &SemanticConvention{Type: "CPUTime", UseSemconvValues: &trueValue}, + want: true, + }, + { + name: "type set, UseSemconvValues false", + sc: &SemanticConvention{Type: "CPUTime", UseSemconvValues: &falseValue}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.sc.ShouldUseSemConvValues() + assert.Equal(t, tt.want, got) + }) + } +} From 4065c3c64ebbee0f8085274174e053fa607e322a Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Wed, 18 Feb 2026 11:08:16 +0000 Subject: [PATCH 14/18] Update tests to cover new functions for inferSemConv pkg and types --- cmd/mdatagen/internal/metadata_test.go | 217 ++++++++++++++++++++----- 1 file changed, 175 insertions(+), 42 deletions(-) diff --git a/cmd/mdatagen/internal/metadata_test.go b/cmd/mdatagen/internal/metadata_test.go index c782fe58a680..c40cfdd93476 100644 --- a/cmd/mdatagen/internal/metadata_test.go +++ b/cmd/mdatagen/internal/metadata_test.go @@ -544,31 +544,31 @@ func TestInferSemConvFromMetricName(t *testing.T) { wantErr bool }{ { - name: "go.goroutine.count", + name: "go.goroutine.count metric returns correct package and type", metric: "go.goroutine.count", wantPkg: "goconv", wantType: "GoroutineCount", }, { - name: "k8s.container.cpu.limit", + name: "k8s.container.cpu.limit metric returns correct package and type", metric: "k8s.container.cpu.limit", wantPkg: "k8sconv", wantType: "ContainerCPULimit", }, { - name: "system.cpu.time", + name: "system.cpu.time metric returns correct package and type", metric: "system.cpu.time", wantPkg: "systemconv", wantType: "CPUTime", }, { - name: "system.cpu.logical.count", + name: "system.cpu.logical.count metric returns correct package and type", metric: "system.cpu.logical.count", wantPkg: "systemconv", wantType: "CPULogicalCount", }, { - name: "system.memory.limit", + name: "system.memory.limit metric returns correct package and type", metric: "system.memory.limit", wantPkg: "systemconv", wantType: "MemoryLimit", @@ -586,19 +586,19 @@ func TestInferSemConvFromMetricName(t *testing.T) { wantType: "DiskIOTime", }, { - name: "system.network.io", + name: "system.network.io metric returns correct package and type", metric: "system.network.io", wantPkg: "systemconv", wantType: "NetworkIO", }, { - name: "system.linux.memory.available", + name: "system.linux.memory.available metric returns correct package and type", metric: "system.linux.memory.available", wantPkg: "systemconv", wantType: "LinuxMemoryAvailable", }, { - name: "system.linux.memory.slab.usage", + name: "system.linux.memory.slab.usage metric returns correct package and type", metric: "system.linux.memory.slab.usage", wantPkg: "systemconv", wantType: "LinuxMemorySlabUsage", @@ -610,19 +610,19 @@ func TestInferSemConvFromMetricName(t *testing.T) { wantType: "Uptime", }, { - name: "system.filesystem.utilization", + name: "system.filesystem.utilization metric returns correct package and type", metric: "system.filesystem.utilization", wantPkg: "systemconv", wantType: "FilesystemUtilization", }, { - name: "system.paging.faults", + name: "system.paging.faults metric returns correct package and type", metric: "system.paging.faults", wantPkg: "systemconv", wantType: "PagingFaults", }, { - name: "system.network.packet.dropped", + name: "system.network.packet.dropped metric returns correct package and type", metric: "system.network.packet.dropped", wantPkg: "systemconv", wantType: "NetworkPacketDropped", @@ -660,50 +660,112 @@ func TestInferSemConvFromMetricName(t *testing.T) { } func TestInferSemConvTypes(t *testing.T) { - md := &Metadata{ - Metrics: map[MetricName]Metric{ - "system.cpu.time": { - Signal: Signal{ - SemanticConvention: &SemanticConvention{ - SemanticConventionRef: "https://example.com", + tests := []struct { + name string + metricName string + md Metadata + wantPkg string + wantType string + wantNil bool + }{ + { + name: "system.cpu.time has valid package and type", + metricName: "system.cpu.time", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "system.cpu.time": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + }, + }, }, }, }, - "system.disk.io": { - Signal: Signal{ - SemanticConvention: &SemanticConvention{ - SemanticConventionRef: "https://example.com", + wantPkg: "systemconv", + wantType: "CPUTime", + }, + { + name: "system.disk.io has valid package and type", + metricName: "system.disk.io", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "system.disk.io": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + }, + }, }, }, }, - // Explicit values should not be overwritten - "system.memory.limit": { - Signal: Signal{ - SemanticConvention: &SemanticConvention{ - SemanticConventionRef: "https://example.com", - Package: "custom", - Type: "Custom", + wantPkg: "systemconv", + wantType: "DiskIO", + }, + { + name: "don't overwrite default values", + metricName: "system.memory.limit", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "system.memory.limit": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + Package: "custom", + Type: "custom", + }, + }, }, }, }, - // No semantic convention, should be left alone - "default.metric": { - Signal: Signal{}, + wantPkg: "custom", + wantType: "custom", + }, + { + name: "no semantic convention doesn't do anything", + metricName: "default.metric", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "default.metric": { + Signal: Signal{}, + }, + }, + }, + wantNil: true, + }, + { + name: "invalid metric name doesn't pupulate pkg and type", + metricName: "invalid", + md: Metadata{ + Metrics: map[MetricName]Metric{ + "invalid": { + Signal: Signal{ + SemanticConvention: &SemanticConvention{ + SemanticConventionRef: "https://example.com", + }, + }, + }, + }, }, + wantPkg: "", + wantType: "", }, } - md.inferSemConvTypes() - - assert.Equal(t, "systemconv", md.Metrics["system.cpu.time"].SemanticConvention.Package) - assert.Equal(t, "CPUTime", md.Metrics["system.cpu.time"].SemanticConvention.Type) - assert.Equal(t, "systemconv", md.Metrics["system.disk.io"].SemanticConvention.Package) - assert.Equal(t, "DiskIO", md.Metrics["system.disk.io"].SemanticConvention.Type) - // Explicit values preserved - assert.Equal(t, "custom", md.Metrics["system.memory.limit"].SemanticConvention.Package) - assert.Equal(t, "Custom", md.Metrics["system.memory.limit"].SemanticConvention.Type) - // No semantic convention - assert.Nil(t, md.Metrics["default.metric"].SemanticConvention) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.md.inferSemConvTypes() + sc := tt.md.Metrics[MetricName(tt.metricName)].SemanticConvention + if tt.wantNil { + assert.Nil(t, sc) + require.Nil(t, sc, "The semantic convention must be nil") + } else { + require.NotNil(t, sc, "The semantic convention must not be nil") + assert.Equal(t, tt.wantPkg, sc.Package) + assert.Equal(t, tt.wantType, sc.Type) + } + }) + } } func TestSemConvImports(t *testing.T) { @@ -828,3 +890,74 @@ func TestShouldUseSemConvValues(t *testing.T) { }) } } + +func TestImportPath(t *testing.T) { + tests := []struct { + name string + sc *SemanticConvention + scv string + want string + }{ + { + name: "Empty package returns empty string", + sc: &SemanticConvention{ + Package: "", + }, + scv: "1.38.0", + want: "", + }, + { + name: "systemconv package", + sc: &SemanticConvention{ + Package: "systemconv", + }, + scv: "1.38.0", + want: "go.opentelemetry.io/otel/semconv/1.38.0/systemconv", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.sc.ImportPath(tt.scv) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestFormatSemConvIdentifiers(t *testing.T) { + tests := []struct { + name string + semconvIdentifier string + want string + wantErr bool + }{ + { + name: "correctly formats semconv go type", + semconvIdentifier: "cpu.time", + want: "CPUTime", + }, + { + name: "correctly formats disk.io to DiskIO", + semconvIdentifier: "disk.io", + want: "DiskIO", + }, + { + name: "returns an error when an empty string is supplied", + semconvIdentifier: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := formatSemConvIdentifier(tt.semconvIdentifier) + if tt.wantErr { + require.Error(t, err, "error is required for empty strings") + assert.Equal(t, "", got, "expect an empty string if an error is returned") + } else { + require.NoError(t, err, "require no error") + assert.Equal(t, tt.want, got) + } + }) + } +} From 96cdcbcc25df7daa7bc5013becc8e54bee8fd9cc Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Wed, 18 Feb 2026 15:18:05 +0000 Subject: [PATCH 15/18] Update metrics test template to add tests for each Attribute --- .../metadata/generated_metrics_test.go | 10 +++ .../metadata/generated_metrics_test.go | 20 +++++ .../metadata/generated_metrics_test.go | 10 +++ .../internal/templates/metrics_test.go.tmpl | 81 +++++++++++-------- 4 files changed, 86 insertions(+), 35 deletions(-) diff --git a/cmd/mdatagen/internal/sampleconnector/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/sampleconnector/internal/metadata/generated_metrics_test.go index bea4933d9162..b0739dc9c5f8 100644 --- a/cmd/mdatagen/internal/sampleconnector/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/sampleconnector/internal/metadata/generated_metrics_test.go @@ -502,3 +502,13 @@ func TestMetricsBuilder(t *testing.T) { }) } } + +func TestAttributeEnumAttrStringInvalid(t *testing.T) { + assert.Equal(t, "", AttributeEnumAttr(999).String()) +} + +func TestMapAttributeEnumAttr(t *testing.T) { + for str, val := range MapAttributeEnumAttr { + assert.Equal(t, str, val.String()) + } +} diff --git a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go index 100aa89b16f2..0ef1640955c6 100644 --- a/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/samplereceiver/internal/metadata/generated_metrics_test.go @@ -678,3 +678,23 @@ func TestMetricsBuilder(t *testing.T) { }) } } + +func TestAttributeEnumAttrStringInvalid(t *testing.T) { + assert.Equal(t, "", AttributeEnumAttr(999).String()) +} + +func TestMapAttributeEnumAttr(t *testing.T) { + for str, val := range MapAttributeEnumAttr { + assert.Equal(t, str, val.String()) + } +} + +func TestAttributeStateStringInvalid(t *testing.T) { + assert.Equal(t, "", AttributeState(999).String()) +} + +func TestMapAttributeState(t *testing.T) { + for str, val := range MapAttributeState { + assert.Equal(t, str, val.String()) + } +} diff --git a/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go index 503028f4f947..f6136f1de9a9 100644 --- a/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go +++ b/cmd/mdatagen/internal/samplescraper/internal/metadata/generated_metrics_test.go @@ -550,3 +550,13 @@ func TestMetricsBuilder(t *testing.T) { }) } } + +func TestAttributeEnumAttrStringInvalid(t *testing.T) { + assert.Equal(t, "", AttributeEnumAttr(999).String()) +} + +func TestMapAttributeEnumAttr(t *testing.T) { + for str, val := range MapAttributeEnumAttr { + assert.Equal(t, str, val.String()) + } +} diff --git a/cmd/mdatagen/internal/templates/metrics_test.go.tmpl b/cmd/mdatagen/internal/templates/metrics_test.go.tmpl index f5ddb62eab55..99d9a4f13d31 100644 --- a/cmd/mdatagen/internal/templates/metrics_test.go.tmpl +++ b/cmd/mdatagen/internal/templates/metrics_test.go.tmpl @@ -15,9 +15,6 @@ import ( {{- end }} "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" - {{- range .SemConvImports }} - {{ .Alias }} "go.opentelemetry.io/otel/semconv/v{{ $.SemConvVersion }}/{{ .Package }}" - {{- end }} ) type testDataSet int @@ -26,9 +23,9 @@ const ( testDataSetDefault testDataSet = iota testDataSetAll testDataSetNone - {{- if $reag }} - testDataSetReag - {{- end }} + {{- if $reag }} + testDataSetReag + {{- end }} ) func TestMetricsBuilder(t *testing.T) { @@ -46,13 +43,13 @@ func TestMetricsBuilder(t *testing.T) { metricsSet: testDataSetAll, resAttrsSet: testDataSetAll, }, - {{- if $reag }} - { - name: "reaggregate_set", - metricsSet: testDataSetReag, - resAttrsSet: testDataSetReag, - }, - {{- end }} + {{- if $reag }} + { + name: "reaggregate_set", + metricsSet: testDataSetReag, + resAttrsSet: testDataSetReag, + }, + {{- end }} { name: "none_set", metricsSet: testDataSetNone, @@ -76,18 +73,18 @@ func TestMetricsBuilder(t *testing.T) { start := pcommon.Timestamp(1_000_000_000) ts := pcommon.Timestamp(1_000_001_000) observedZapCore, observedLogs := observer.New(zap.WarnLevel) - {{- if or isReceiver isScraper isConnector }} - settings := {{ .Status.Class }}test.NewNopSettings({{ .Status.Class }}test.NopType) - {{- end }} + {{- if or isReceiver isScraper isConnector }} + settings := {{ .Status.Class }}test.NewNopSettings({{ .Status.Class }}test.NopType) + {{- end }} settings.Logger = zap.New(observedZapCore) mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, tt.name), settings, WithStartTime(start)) - {{- if $reag }} - aggMap := make(map[string]string) // contains the aggregation strategies for each metric name - {{- range $name, $_ := .Metrics }} - aggMap["{{ $name.Render }}"] = mb.metric{{ $name.Render }}.config.AggregationStrategy - {{- end }} - {{- end }} + {{- if $reag }} + aggMap := make(map[string]string) // contains the aggregation strategies for each metric name + {{- range $name, $_ := .Metrics }} + aggMap["{{ $name.Render }}"] = mb.metric{{ $name.Render }}.config.AggregationStrategy + {{- end }} + {{- end }} expectedWarnings := 0 {{- range $name, $metric := .Metrics }} @@ -131,13 +128,13 @@ func TestMetricsBuilder(t *testing.T) { {{- end }} {{- end }} - {{- if $reag }} - if tt.metricsSet != testDataSetReag { - assert.Equal(t, expectedWarnings, observedLogs.Len()) - } - {{- else }} - assert.Equal(t, expectedWarnings, observedLogs.Len()) - {{- end }} + {{- if $reag }} + if tt.metricsSet != testDataSetReag { + assert.Equal(t, expectedWarnings, observedLogs.Len()) + } + {{- else }} + assert.Equal(t, expectedWarnings, observedLogs.Len()) + {{- end }} defaultMetricsCount := 0 allMetricsCount := 0 @@ -330,15 +327,15 @@ func TestMetricsBuilder(t *testing.T) { {{- end }} } {{- else }} - assert.False(t, validatedMetrics["{{ $name }}"], "Found a duplicate in the metrics slice: {{ $name }}") + assert.False(t, validatedMetrics["{{ $name }}"], "Found a duplicate in the metrics slice: {{ $name }}") validatedMetrics["{{ $name }}"] = true assert.Equal(t, pmetric.MetricType{{ $metric.Data.Type }}, ms.At(i).Type()) assert.Equal(t, 1, ms.At(i).{{ $metric.Data.Type }}().DataPoints().Len()) - {{- if $metric.SemanticConvention.ShouldUseSemConvValues }} - assert.Equal(t, metricSemConv{{ $name.Render }}.Description(), ms.At(i).Description()) - {{- else }} - assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) - {{- end }} + {{- if $metric.SemanticConvention.ShouldUseSemConvValues }} + assert.Equal(t, metricSemConv{{ $name.Render }}.Description(), ms.At(i).Description()) + {{- else }} + assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) + {{- end }} {{- if len $metric.Unit}} assert.Equal(t, "{{ $metric.Unit }}", ms.At(i).Unit()) {{- else }} @@ -379,3 +376,17 @@ func TestMetricsBuilder(t *testing.T) { }) } } + +{{ range $name, $info := .Attributes }} +{{- if $info.Enum }} +func TestAttribute{{ $name.Render }}StringInvalid(t *testing.T) { + assert.Equal(t, "", Attribute{{ $name.Render }}(999).String()) +} + +func TestMapAttribute{{ $name.Render }}(t *testing.T) { + for str, val := range MapAttribute{{ $name.Render }} { + assert.Equal(t, str, val.String()) + } +} +{{- end }} +{{ end }} From 271da4ed0cb8e62fed2a0114ae98c476706831d7 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Wed, 18 Feb 2026 16:03:48 +0000 Subject: [PATCH 16/18] Use assert.Empty not assert.Equal for empty string test --- cmd/mdatagen/internal/metadata_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/mdatagen/internal/metadata_test.go b/cmd/mdatagen/internal/metadata_test.go index d582401c278f..99f8f466978f 100644 --- a/cmd/mdatagen/internal/metadata_test.go +++ b/cmd/mdatagen/internal/metadata_test.go @@ -977,7 +977,7 @@ func TestFormatSemConvIdentifiers(t *testing.T) { got, err := formatSemConvIdentifier(tt.semconvIdentifier) if tt.wantErr { require.Error(t, err, "error is required for empty strings") - assert.Equal(t, "", got, "expect an empty string if an error is returned") + assert.Empty(t, got, "expect an empty string if an error is returned") } else { require.NoError(t, err, "require no error") assert.Equal(t, tt.want, got) From 5fc9a95a3b79e7fa62252627ca4fefc2b8440b17 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Tue, 24 Feb 2026 13:57:02 +0000 Subject: [PATCH 17/18] Update semconv url underscore test to include package and type --- cmd/mdatagen/internal/loader_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go index 6183fddce7ce..09dd88354f6f 100644 --- a/cmd/mdatagen/internal/loader_test.go +++ b/cmd/mdatagen/internal/loader_test.go @@ -750,6 +750,8 @@ func TestLoadMetadata(t *testing.T) { Stability: component.StabilityLevelDevelopment, SemanticConvention: &SemanticConvention{ SemanticConventionRef: "https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemdiskio_time", + Package: "systemconv", + Type: "DiskIOTime", }, }, Unit: strPtr("s"), From 1c0aa264a897919931ebb87502c5393d7b271496 Mon Sep 17 00:00:00 2001 From: Donal O'Sullivan Date: Tue, 24 Feb 2026 16:11:18 +0000 Subject: [PATCH 18/18] Remove the package, type and use_semconv_values from the metadat schema These have been removed as we don't want the consumer to have to manually specify this, mdatagen can infer thse directly from the semantic_convention ref url. --- cmd/mdatagen/internal/metadata.go | 30 ++++++++-------- cmd/mdatagen/internal/metadata_test.go | 34 +------------------ .../internal/samplereceiver/metadata.yaml | 4 +-- cmd/mdatagen/metadata-schema.yaml | 7 ---- 4 files changed, 16 insertions(+), 59 deletions(-) diff --git a/cmd/mdatagen/internal/metadata.go b/cmd/mdatagen/internal/metadata.go index ca0102bea067..e59b673ca5f5 100644 --- a/cmd/mdatagen/internal/metadata.go +++ b/cmd/mdatagen/internal/metadata.go @@ -528,9 +528,12 @@ func (mvt ValueType) Primitive() string { type SemanticConvention struct { SemanticConventionRef string `mapstructure:"ref"` - Package string `mapstructure:"package"` - Type string `mapstructure:"type"` - UseSemconvValues *bool `mapstructure:"use_semconv_values"` + // Package is the inferred semconv Go package name (e.g. "systemconv"). + // This field is computed from the metric name, not user-specified. + Package string + // Type is the inferred semconv Go type name (e.g. "CPUTime"). + // This field is computed from the metric name, not user-specified. + Type string } type Warnings struct { @@ -714,10 +717,7 @@ func (s *SemanticConvention) ShouldUseSemConvValues() bool { if s == nil || s.Type == "" { return false } - if s.UseSemconvValues == nil { - return true - } - return *s.UseSemconvValues + return true } func (s *SemanticConvention) ImportPath(semConvVersion string) string { @@ -728,22 +728,20 @@ func (s *SemanticConvention) ImportPath(semConvVersion string) string { } // inferSemConvTypes auto-populates Package and Type on any SemanticConvention -// that has a ref URL but is missing Package/Type, by inferring them from the metric name. +// that has a ref URL, by inferring them from the metric name. func (md *Metadata) inferSemConvTypes() { for name, m := range md.Metrics { sc := m.SemanticConvention if sc == nil || sc.SemanticConventionRef == "" { continue } - if sc.Package == "" && sc.Type == "" { - pkg, typeName, err := InferSemConvFromMetricName(string(name)) - if err != nil { - // If inference fails, leave them empty; validation will catch it. - continue - } - sc.Package = pkg - sc.Type = typeName + pkg, typeName, err := InferSemConvFromMetricName(string(name)) + if err != nil { + // If inference fails, leave them empty; validation will catch it. + continue } + sc.Package = pkg + sc.Type = typeName } } diff --git a/cmd/mdatagen/internal/metadata_test.go b/cmd/mdatagen/internal/metadata_test.go index 99f8f466978f..af1843406574 100644 --- a/cmd/mdatagen/internal/metadata_test.go +++ b/cmd/mdatagen/internal/metadata_test.go @@ -726,25 +726,6 @@ func TestInferSemConvTypes(t *testing.T) { wantPkg: "systemconv", wantType: "DiskIO", }, - { - name: "don't overwrite default values", - metricName: "system.memory.limit", - md: Metadata{ - Metrics: map[MetricName]Metric{ - "system.memory.limit": { - Signal: Signal{ - SemanticConvention: &SemanticConvention{ - SemanticConventionRef: "https://example.com", - Package: "custom", - Type: "custom", - }, - }, - }, - }, - }, - wantPkg: "custom", - wantType: "custom", - }, { name: "no semantic convention doesn't do anything", metricName: "default.metric", @@ -872,9 +853,6 @@ func TestSemConvImports(t *testing.T) { } func TestShouldUseSemConvValues(t *testing.T) { - trueValue := true - falseValue := false - tests := []struct { name string sc *SemanticConvention @@ -891,20 +869,10 @@ func TestShouldUseSemConvValues(t *testing.T) { want: false, }, { - name: "type set, UseSemconvValues nil (default true)", + name: "type set returns true", sc: &SemanticConvention{Type: "CPUTime"}, want: true, }, - { - name: "type set, UseSemconvValues true", - sc: &SemanticConvention{Type: "CPUTime", UseSemconvValues: &trueValue}, - want: true, - }, - { - name: "type set, UseSemconvValues false", - sc: &SemanticConvention{Type: "CPUTime", UseSemconvValues: &falseValue}, - want: false, - }, } for _, tt := range tests { diff --git a/cmd/mdatagen/internal/samplereceiver/metadata.yaml b/cmd/mdatagen/internal/samplereceiver/metadata.yaml index e8bec9453528..5d107105a85c 100644 --- a/cmd/mdatagen/internal/samplereceiver/metadata.yaml +++ b/cmd/mdatagen/internal/samplereceiver/metadata.yaml @@ -296,8 +296,6 @@ metrics: attributes: [cpu, state] semantic_convention: ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemcputime - package: systemconv - type: CPUTime system.memory.limit: enabled: true @@ -308,7 +306,7 @@ metrics: value_type: int monotonic: false aggregation_temporality: cumulative - # package and type are intentionally omitted to test auto-inference from metric name + # package and type are auto-inferred from the metric name semantic_convention: ref: https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/system/system-metrics.md#metric-systemmemorylimit diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index 78df6f1a382f..ef158e3f4e31 100644 --- a/cmd/mdatagen/metadata-schema.yaml +++ b/cmd/mdatagen/metadata-schema.yaml @@ -170,15 +170,8 @@ metrics: # Optional: the reference to a semantic convention. # When ref is provided, package and type are auto-inferred from the metric name # (e.g. "system.cpu.time" → package "systemconv", type "CPUTime"). - # Explicit package/type can be specified to override the inferred values. semantic_convention: ref: - # Optional: semantic convention Go package name. Inferred from metric name if omitted (e.g. "systemconv"). - package: - # Optional: semantic convention Go type name. Inferred from metric name if omitted (e.g. "CPUTime"). - type: - # Optional: whether to use semantic convention Name/Description/Unit in generated code. Defaults to true. - use_semconv_values: # Optional: map of event names with the key being the event name and value # being described below.