diff --git a/.chloggen/45396-winservice-enable-reagg.yaml b/.chloggen/45396-winservice-enable-reagg.yaml new file mode 100644 index 0000000000000..7f7379879bdf3 --- /dev/null +++ b/.chloggen/45396-winservice-enable-reagg.yaml @@ -0,0 +1,27 @@ +# 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/filelog) +component: receiver/windowsservice + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Enables dynamic metric reaggregation in the Splunk Enterprise receiver. This does not break existing configuration files. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [45396] + +# (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: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# 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, api] diff --git a/receiver/windowsservicereceiver/documentation.md b/receiver/windowsservicereceiver/documentation.md index 0b1a88d99030d..18aefb082a26c 100644 --- a/receiver/windowsservicereceiver/documentation.md +++ b/receiver/windowsservicereceiver/documentation.md @@ -27,5 +27,5 @@ Gauge values map to service status as follows: 0 - Failed to retrieve service st | Name | Description | Values | Requirement Level | | ---- | ----------- | ------ | -------- | -| name | The name of the windows Service being reported. | Any Str | Recommended | -| startup_mode | Startup mode of Windows Service | Str: ``boot_start``, ``system_start``, ``auto_start``, ``demand_start``, ``disabled`` | Recommended | +| name | The name of the windows Service being reported. | Any Str | Required | +| startup_mode | Startup mode of Windows Service | Str: ``boot_start``, ``system_start``, ``auto_start``, ``demand_start``, ``disabled`` | Required | diff --git a/receiver/windowsservicereceiver/internal/metadata/generated_config.go b/receiver/windowsservicereceiver/internal/metadata/generated_config.go index 9eaad77f8710d..6c9c0e05d250e 100644 --- a/receiver/windowsservicereceiver/internal/metadata/generated_config.go +++ b/receiver/windowsservicereceiver/internal/metadata/generated_config.go @@ -3,6 +3,9 @@ package metadata import ( + "fmt" + "slices" + "go.opentelemetry.io/collector/confmap" ) @@ -10,6 +13,11 @@ import ( type MetricConfig struct { Enabled bool `mapstructure:"enabled"` enabledSetByUser bool + + AggregationStrategy string `mapstructure:"aggregation_strategy"` + EnabledAttributes []string `mapstructure:"attributes"` + definedAttributes []string + requiredAttributes []string } func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { @@ -21,11 +29,34 @@ func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { if err != nil { return err } + for _, val := range ms.EnabledAttributes { + if !slices.Contains(ms.definedAttributes, val) { + return fmt.Errorf("%v is not defined in metadata.yaml", val) + } + } + + for _, val := range ms.requiredAttributes { + if !slices.Contains(ms.EnabledAttributes, val) { + return fmt.Errorf("`attributes` field must contain required attribute: %v", val) + } + } + + if ms.AggregationStrategy != AggregationStrategySum && + ms.AggregationStrategy != AggregationStrategyAvg && + ms.AggregationStrategy != AggregationStrategyMin && + ms.AggregationStrategy != AggregationStrategyMax { + return fmt.Errorf("invalid aggregation strategy set: '%v'", ms.AggregationStrategy) + } ms.enabledSetByUser = parser.IsSet("enabled") return nil } +// AttributeConfig holds configuration information for a particular metric. +type AttributeConfig struct { + Enabled bool `mapstructure:"enabled"` +} + // MetricsConfig provides config for windowsservice metrics. type MetricsConfig struct { WindowsServiceStatus MetricConfig `mapstructure:"windows.service.status"` @@ -35,6 +66,11 @@ func DefaultMetricsConfig() MetricsConfig { return MetricsConfig{ WindowsServiceStatus: MetricConfig{ Enabled: true, + + AggregationStrategy: AggregationStrategyAvg, + requiredAttributes: []string{"name", "startup_mode"}, + definedAttributes: []string{"name", "startup_mode"}, + EnabledAttributes: []string{"name", "startup_mode"}, }, } } diff --git a/receiver/windowsservicereceiver/internal/metadata/generated_config_test.go b/receiver/windowsservicereceiver/internal/metadata/generated_config_test.go index a2c4acac2b970..8a580a6fb7f80 100644 --- a/receiver/windowsservicereceiver/internal/metadata/generated_config_test.go +++ b/receiver/windowsservicereceiver/internal/metadata/generated_config_test.go @@ -26,7 +26,11 @@ func TestMetricsBuilderConfig(t *testing.T) { name: "all_set", want: MetricsBuilderConfig{ Metrics: MetricsConfig{ - WindowsServiceStatus: MetricConfig{Enabled: true}, + WindowsServiceStatus: MetricConfig{ + Enabled: true, + AggregationStrategy: AggregationStrategyAvg, + EnabledAttributes: []string{"name", "startup_mode"}, + }, }, }, }, @@ -34,7 +38,11 @@ func TestMetricsBuilderConfig(t *testing.T) { name: "none_set", want: MetricsBuilderConfig{ Metrics: MetricsConfig{ - WindowsServiceStatus: MetricConfig{Enabled: false}, + WindowsServiceStatus: MetricConfig{ + Enabled: false, + AggregationStrategy: AggregationStrategyAvg, + EnabledAttributes: []string{"name", "startup_mode"}, + }, }, }, }, diff --git a/receiver/windowsservicereceiver/internal/metadata/generated_metrics.go b/receiver/windowsservicereceiver/internal/metadata/generated_metrics.go index 743a0085af004..4e81414af15b8 100644 --- a/receiver/windowsservicereceiver/internal/metadata/generated_metrics.go +++ b/receiver/windowsservicereceiver/internal/metadata/generated_metrics.go @@ -3,6 +3,7 @@ package metadata import ( + "slices" "time" "go.opentelemetry.io/collector/component" @@ -11,6 +12,13 @@ import ( "go.opentelemetry.io/collector/receiver" ) +const ( + AggregationStrategySum = "sum" + AggregationStrategyAvg = "avg" + AggregationStrategyMin = "min" + AggregationStrategyMax = "max" +) + // AttributeStartupMode specifies the value startup_mode attribute. type AttributeStartupMode int @@ -64,9 +72,10 @@ type metricInfo struct { } type metricWindowsServiceStatus 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. + 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 } // init fills windows.service.status metric with initial data. @@ -82,12 +91,44 @@ func (m *metricWindowsServiceStatus) recordDataPoint(start pcommon.Timestamp, ts if !m.config.Enabled { return } - dp := m.data.Gauge().DataPoints().AppendEmpty() + + dp := pmetric.NewNumberDataPoint() dp.SetStartTimestamp(start) dp.SetTimestamp(ts) + if slices.Contains(m.config.EnabledAttributes, "name") { + dp.Attributes().PutStr("name", nameAttributeValue) + } + if slices.Contains(m.config.EnabledAttributes, "startup_mode") { + dp.Attributes().PutStr("startup_mode", startupModeAttributeValue) + } + + var s string + dps := m.data.Gauge().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) - dp.Attributes().PutStr("name", nameAttributeValue) - dp.Attributes().PutStr("startup_mode", startupModeAttributeValue) + 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. @@ -100,6 +141,11 @@ func (m *metricWindowsServiceStatus) updateCapacity() { // emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. func (m *metricWindowsServiceStatus) emit(metrics pmetric.MetricSlice) { if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + if m.config.AggregationStrategy == AggregationStrategyAvg { + for i, aggCount := range m.aggDataPoints { + m.data.Gauge().DataPoints().At(i).SetIntValue(m.data.Gauge().DataPoints().At(i).IntValue() / aggCount) + } + } m.updateCapacity() m.data.MoveTo(metrics.AppendEmpty()) m.init() diff --git a/receiver/windowsservicereceiver/internal/metadata/generated_metrics_test.go b/receiver/windowsservicereceiver/internal/metadata/generated_metrics_test.go index d832eea854fbf..a98e9eac6454b 100644 --- a/receiver/windowsservicereceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/windowsservicereceiver/internal/metadata/generated_metrics_test.go @@ -19,6 +19,7 @@ const ( testDataSetDefault testDataSet = iota testDataSetAll testDataSetNone + testDataSetReag ) func TestMetricsBuilder(t *testing.T) { @@ -36,6 +37,11 @@ func TestMetricsBuilder(t *testing.T) { metricsSet: testDataSetAll, resAttrsSet: testDataSetAll, }, + { + name: "reaggregate_set", + metricsSet: testDataSetReag, + resAttrsSet: testDataSetReag, + }, { name: "none_set", metricsSet: testDataSetNone, @@ -51,9 +57,13 @@ func TestMetricsBuilder(t *testing.T) { settings := receivertest.NewNopSettings(receivertest.NopType) settings.Logger = zap.New(observedZapCore) mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, tt.name), settings, WithStartTime(start)) + aggMap := make(map[string]string) // contains the aggregation strategies for each metric name + aggMap["WindowsServiceStatus"] = mb.metricWindowsServiceStatus.config.AggregationStrategy expectedWarnings := 0 - assert.Equal(t, expectedWarnings, observedLogs.Len()) + if tt.metricsSet != testDataSetReag { + assert.Equal(t, expectedWarnings, observedLogs.Len()) + } defaultMetricsCount := 0 allMetricsCount := 0 @@ -61,6 +71,9 @@ func TestMetricsBuilder(t *testing.T) { defaultMetricsCount++ allMetricsCount++ mb.RecordWindowsServiceStatusDataPoint(ts, 1, "name-val", AttributeStartupModeBootStart) + if tt.name == "reaggregate_set" { + mb.RecordWindowsServiceStatusDataPoint(ts, 3, "name-val", AttributeStartupModeBootStart) + } res := pcommon.NewResource() metrics := mb.Emit(WithResource(res)) @@ -85,23 +98,50 @@ func TestMetricsBuilder(t *testing.T) { for i := 0; i < ms.Len(); i++ { switch ms.At(i).Name() { case "windows.service.status": - assert.False(t, validatedMetrics["windows.service.status"], "Found a duplicate in the metrics slice: windows.service.status") - validatedMetrics["windows.service.status"] = true - assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) - assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) - assert.Equal(t, "Gauge value containing service status as an integer value.", ms.At(i).Description()) - assert.Equal(t, "{status}", ms.At(i).Unit()) - dp := ms.At(i).Gauge().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()) - attrVal, ok := dp.Attributes().Get("name") - assert.True(t, ok) - assert.Equal(t, "name-val", attrVal.Str()) - attrVal, ok = dp.Attributes().Get("startup_mode") - assert.True(t, ok) - assert.Equal(t, "boot_start", attrVal.Str()) + if tt.name != "reaggregate_set" { + assert.False(t, validatedMetrics["windows.service.status"], "Found a duplicate in the metrics slice: windows.service.status") + validatedMetrics["windows.service.status"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Gauge value containing service status as an integer value.", ms.At(i).Description()) + assert.Equal(t, "{status}", ms.At(i).Unit()) + dp := ms.At(i).Gauge().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()) + attrVal, ok := dp.Attributes().Get("name") + assert.True(t, ok) + assert.Equal(t, "name-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("startup_mode") + assert.True(t, ok) + assert.Equal(t, "boot_start", attrVal.Str()) + } else { + assert.False(t, validatedMetrics["windows.service.status"], "Found a duplicate in the metrics slice: windows.service.status") + validatedMetrics["windows.service.status"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Gauge value containing service status as an integer value.", ms.At(i).Description()) + assert.Equal(t, "{status}", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + switch aggMap["windows.service.status"] { + 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()) + } + _, ok := dp.Attributes().Get("name") + assert.True(t, ok) + _, ok = dp.Attributes().Get("startup_mode") + assert.True(t, ok) + } } } }) diff --git a/receiver/windowsservicereceiver/internal/metadata/testdata/config.yaml b/receiver/windowsservicereceiver/internal/metadata/testdata/config.yaml index a8dee4198bdff..0b4c50433f5ab 100644 --- a/receiver/windowsservicereceiver/internal/metadata/testdata/config.yaml +++ b/receiver/windowsservicereceiver/internal/metadata/testdata/config.yaml @@ -3,7 +3,14 @@ all_set: metrics: windows.service.status: enabled: true + attributes: ["name","startup_mode"] +reaggregate_set: + metrics: + windows.service.status: + enabled: true + attributes: ["name","startup_mode"] none_set: metrics: windows.service.status: enabled: false + attributes: ["name","startup_mode"] diff --git a/receiver/windowsservicereceiver/metadata.yaml b/receiver/windowsservicereceiver/metadata.yaml index 98a1d5a0bf7e4..ce29b7e24dd8b 100644 --- a/receiver/windowsservicereceiver/metadata.yaml +++ b/receiver/windowsservicereceiver/metadata.yaml @@ -9,14 +9,18 @@ status: active: [pjanotti, shalper2] unsupported_platforms: [darwin, linux] +reaggregation_enabled: true + attributes: name: description: The name of the windows Service being reported. type: string + requirement_level: required startup_mode: description: Startup mode of Windows Service enum: [boot_start, system_start, auto_start, demand_start, disabled] type: string + requirement_level: required metrics: windows.service.status: