diff --git a/.chloggen/httpcheck-validation-result-metric.yaml b/.chloggen/httpcheck-validation-result-metric.yaml new file mode 100644 index 0000000000000..76f91bc7faae4 --- /dev/null +++ b/.chloggen/httpcheck-validation-result-metric.yaml @@ -0,0 +1,36 @@ +# 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/http_check + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add structured validation result metrics with per-validation attributes for response validation monitoring. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [44662] + +# (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: | + Introduces `httpcheck.validation.result`, which emits one datapoint per validation rule with structured attributes including: + - `validation.type` + - `validation.path` + - `validation.expected` + - `validation.result` + + This allows users to identify exactly which validation rule passed or failed when multiple validations are configured for a single endpoint. + + Legacy metrics (`httpcheck.validation.passed` and `httpcheck.validation.failed`) continue to work unchanged for backward compatibility. + +# 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] diff --git a/receiver/httpcheckreceiver/README.md b/receiver/httpcheckreceiver/README.md index 78c6ad74a4999..c7423019018cc 100644 --- a/receiver/httpcheckreceiver/README.md +++ b/receiver/httpcheckreceiver/README.md @@ -18,16 +18,16 @@ The HTTP Check Receiver can be used for synthetic checks against HTTP endpoints. [k8s]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-k8s -This receiver will make a request to the specified `endpoint` using the -configured `method`. This scraper generates a metric with a label for each HTTP response status class with a value of `1` if the status code matches the +This receiver makes a request to the specified `endpoint` using the +configured `method`. This scraper generates metrics for each HTTP response status class with a value of `1` if the status code matches the class. For example, the following metrics will be generated if the endpoint returned a `200`: ``` -httpcheck.status{http.status_class:1xx, http.status_code:200,...} = 0 +httpcheck.status{http.status_class:1xx,...} = 0 httpcheck.status{http.status_class:2xx, http.status_code:200,...} = 1 -httpcheck.status{http.status_class:3xx, http.status_code:200,...} = 0 -httpcheck.status{http.status_class:4xx, http.status_code:200,...} = 0 -httpcheck.status{http.status_class:5xx, http.status_code:200,...} = 0 +httpcheck.status{http.status_class:3xx,...} = 0 +httpcheck.status{http.status_class:4xx,...} = 0 +httpcheck.status{http.status_class:5xx,...} = 0 ``` For HTTPS endpoints, the receiver can collect TLS certificate metrics including the time remaining until certificate expiry. This allows monitoring of certificate expiration alongside HTTP availability. Note that TLS certificate metrics are disabled by default and must be explicitly enabled in the metrics configuration. @@ -40,8 +40,8 @@ For HTTPS endpoints, the receiver can collect TLS certificate metrics including The following configuration settings are available: - `targets` (required): The list of targets to be monitored. -- `collection_interval` (optional, default = `60s`): This receiver collects metrics on an interval. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. -- `initial_delay` (optional, default = `1s`): defines how long this receiver waits before starting. +- `collection_interval` (optional, default = `60s`): This receiver collects metrics at a configured interval. +- `initial_delay` (optional, default = `1s`): Defines how long this receiver waits before starting. Each target has the following properties: @@ -91,11 +91,12 @@ receivers: ``` These metrics provide detailed timing information for different phases of the HTTP request: -- `dns.lookup.duration`: Time spent performing DNS lookup -- `client.connection.duration`: Time spent establishing TCP connection -- `tls.handshake.duration`: Time spent performing TLS handshake (HTTPS only) -- `client.request.duration`: Time spent sending the HTTP request -- `response.duration`: Time spent receiving the HTTP response +- `httpcheck.duration`: Total time spent completing the HTTP check +- `httpcheck.dns.lookup.duration`: Time spent performing DNS lookup +- `httpcheck.client.connection.duration`: Time spent establishing TCP connection +- `httpcheck.tls.handshake.duration`: Time spent performing TLS handshake (HTTPS only) +- `httpcheck.client.request.duration`: Time spent sending the HTTP request +- `httpcheck.response.duration`: Time spent receiving the HTTP response #### Response Validation Metrics @@ -105,15 +106,93 @@ For API monitoring and health checks, response validation metrics are available: receivers: http_check: metrics: - httpcheck.validation.passed: - enabled: true - httpcheck.validation.failed: + httpcheck.validation.outcome: enabled: true httpcheck.response.size: enabled: true ``` -These metrics track validation results with `validation.type` attribute indicating the validation type (contains, json_path, size, regex). +The `httpcheck.validation.outcome` metric emits one data point per validation rule execution and provides structured attributes for detailed querying and alerting. + +| Attribute | Description | Example Values | +|-----------|-------------|----------------| +| `http.url` | Full HTTP request URL | `https://api.example.com/health` | +| `httpcheck.validation.type` | Type of validation performed | `json_path`, `contains`, `regex`, `size`, `not_contains` | +| `httpcheck.validation.target` | Validation target being evaluated | `system_1`, `healthy`, `^ok$`, `max_size` | +| `httpcheck.validation.outcome` | Pass/fail result of the validation | `passed`, `failed` | + +**Example Output:** + +```text +httpcheck.validation.outcome{ + http.url="https://api.example.com/health", + httpcheck.validation.type="json_path", + httpcheck.validation.target="system_1", + httpcheck.validation.outcome="passed" +} = 1 + +httpcheck.validation.outcome{ + http.url="https://api.example.com/health", + httpcheck.validation.type="json_path", + httpcheck.validation.target="system_3", + httpcheck.validation.outcome="failed" +} = 1 +``` + +This allows you to: +- Query specific validation paths: `httpcheck.validation.outcome{httpcheck.validation.target="system_1"}` +- Monitor specific validation types: `httpcheck.validation.outcome{httpcheck.validation.type="json_path", httpcheck.validation.outcome="failed"}` + +### Example: Multiple JSON Path Validations + +Given the following API response: + +```json +{ + "system_1": true, + "system_2": true, + "system_3": false +} +``` + +And the following validations: + +```yaml +validations: + - json_path: "system_1" + equals: "true" + - json_path: "system_2" + equals: "true" + - json_path: "system_3" + equals: "true" +``` + +The receiver emits: + +```text +httpcheck.validation.outcome{ + http.url="https://api.example.com/health", + httpcheck.validation.type="json_path", + httpcheck.validation.target="system_1", + httpcheck.validation.outcome="passed" +} = 1 + +httpcheck.validation.outcome{ + http.url="https://api.example.com/health", + httpcheck.validation.type="json_path", + httpcheck.validation.target="system_2", + httpcheck.validation.outcome="passed" +} = 1 + +httpcheck.validation.outcome{ + http.url="https://api.example.com/health", + httpcheck.validation.type="json_path", + httpcheck.validation.target="system_3", + httpcheck.validation.outcome="failed" +} = 1 +``` + +This allows you to immediately identify that `system_3` failed, without any additional parsing or aggregation. ### Request Body Support @@ -161,17 +240,17 @@ receivers: # String matching - contains: "healthy" - not_contains: "error" - + # JSON path validation using gjson syntax - - json_path: "$.status" + - json_path: "status" equals: "ok" - - json_path: "$.services[*].status" + - json_path: "services.*.status" equals: "up" - + # Response size validation (bytes) - max_size: 1024 - min_size: 10 - + # Regex validation - regex: "^HTTP/[0-9.]+ 200" ``` @@ -182,7 +261,6 @@ receivers: - `max_size` / `min_size`: Response body size limits - `regex`: Regular expression matching - ### Example Configuration ```yaml @@ -199,13 +277,14 @@ receivers: enabled: true httpcheck.client.request.duration: enabled: true + httpcheck.duration: + enabled: true httpcheck.response.duration: enabled: true httpcheck.tls.cert_remaining: enabled: true - httpcheck.validation.passed: - enabled: true - httpcheck.validation.failed: + # Validation outcome metric + httpcheck.validation.outcome: enabled: true httpcheck.response.size: enabled: true @@ -230,7 +309,7 @@ receivers: endpoint: "https://api.example.com/health" validations: - contains: "healthy" - - json_path: "$.status" + - json_path: "status" equals: "ok" - max_size: 1024 exporters: diff --git a/receiver/httpcheckreceiver/documentation.md b/receiver/httpcheckreceiver/documentation.md index e7702cfefdef9..ea8f271fa33ce 100644 --- a/receiver/httpcheckreceiver/documentation.md +++ b/receiver/httpcheckreceiver/documentation.md @@ -170,9 +170,9 @@ Time spent performing TLS handshake with the endpoint. | ---- | ----------- | ------ | ----------------- | ------------------- | | http.url | Full HTTP request URL. | Any Str | Recommended | - | -### httpcheck.validation.failed +### httpcheck.validation.outcome -Number of response validations that failed. +Result of a response validation (1 for each validation, with outcome attribute). | Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | Stability | | ---- | ----------- | ---------- | ----------------------- | --------- | --------- | @@ -183,19 +183,6 @@ Number of response validations that failed. | Name | Description | Values | Requirement Level | Semantic Convention | | ---- | ----------- | ------ | ----------------- | ------------------- | | http.url | Full HTTP request URL. | Any Str | Recommended | - | -| validation.type | Type of validation performed (contains, json_path, size, regex) | Any Str | Recommended | - | - -### httpcheck.validation.passed - -Number of response validations that passed. - -| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | Stability | -| ---- | ----------- | ---------- | ----------------------- | --------- | --------- | -| {validation} | Sum | Int | Cumulative | false | Development | - -#### Attributes - -| Name | Description | Values | Requirement Level | Semantic Convention | -| ---- | ----------- | ------ | ----------------- | ------------------- | -| http.url | Full HTTP request URL. | Any Str | Recommended | - | -| validation.type | Type of validation performed (contains, json_path, size, regex) | Any Str | Recommended | - | +| httpcheck.validation.type | Type of validation performed (json_path, contains, regex, size, not_contains) | Any Str | Recommended | - | +| httpcheck.validation.target | The validation expression being evaluated (JSONPath, contains string, regex pattern, size constraint) - statically configured | Any Str | Recommended | - | +| httpcheck.validation.outcome | The result of the validation (passed or failed) | Any Str | Recommended | - | diff --git a/receiver/httpcheckreceiver/internal/metadata/generated_config.go b/receiver/httpcheckreceiver/internal/metadata/generated_config.go index 8eecaef747da3..9ee1264994ccc 100644 --- a/receiver/httpcheckreceiver/internal/metadata/generated_config.go +++ b/receiver/httpcheckreceiver/internal/metadata/generated_config.go @@ -496,73 +496,26 @@ func (ms *HttpcheckTLSHandshakeDurationMetricConfig) Validate() error { return nil } -// HttpcheckValidationFailedMetricAttributeKey specifies the key of an attribute for the httpcheck.validation.failed metric. -type HttpcheckValidationFailedMetricAttributeKey string +// HttpcheckValidationOutcomeMetricAttributeKey specifies the key of an attribute for the httpcheck.validation.outcome metric. +type HttpcheckValidationOutcomeMetricAttributeKey string const ( - HttpcheckValidationFailedMetricAttributeKeyHTTPURL HttpcheckValidationFailedMetricAttributeKey = "http.url" - HttpcheckValidationFailedMetricAttributeKeyValidationType HttpcheckValidationFailedMetricAttributeKey = "validation.type" + HttpcheckValidationOutcomeMetricAttributeKeyHTTPURL HttpcheckValidationOutcomeMetricAttributeKey = "http.url" + HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationType HttpcheckValidationOutcomeMetricAttributeKey = "httpcheck.validation.type" + HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationTarget HttpcheckValidationOutcomeMetricAttributeKey = "httpcheck.validation.target" + HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationOutcome HttpcheckValidationOutcomeMetricAttributeKey = "httpcheck.validation.outcome" ) -// HttpcheckValidationFailedMetricConfig provides config for the httpcheck.validation.failed metric. -type HttpcheckValidationFailedMetricConfig struct { +// HttpcheckValidationOutcomeMetricConfig provides config for the httpcheck.validation.outcome metric. +type HttpcheckValidationOutcomeMetricConfig struct { Enabled bool `mapstructure:"enabled"` enabledSetByUser bool - AggregationStrategy string `mapstructure:"aggregation_strategy"` - EnabledAttributes []HttpcheckValidationFailedMetricAttributeKey `mapstructure:"attributes"` -} - -func (ms *HttpcheckValidationFailedMetricConfig) Unmarshal(parser *confmap.Conf) error { - if parser == nil { - return nil - } - - err := parser.Unmarshal(ms) - if err != nil { - return err - } - - ms.enabledSetByUser = parser.IsSet("enabled") - return nil -} - -func (ms *HttpcheckValidationFailedMetricConfig) Validate() error { - for _, val := range ms.EnabledAttributes { - switch val { - case HttpcheckValidationFailedMetricAttributeKeyHTTPURL, HttpcheckValidationFailedMetricAttributeKeyValidationType: - default: - return fmt.Errorf("metric httpcheck.validation.failed doesn't have an attribute %v, valid attributes: [http.url, validation.type]", val) - } - } - - switch ms.AggregationStrategy { - case AggregationStrategySum, AggregationStrategyAvg, AggregationStrategyMin, AggregationStrategyMax: - default: - return fmt.Errorf("invalid aggregation strategy %q, valid strategies: [%s, %s, %s, %s]", ms.AggregationStrategy, AggregationStrategySum, AggregationStrategyAvg, AggregationStrategyMin, AggregationStrategyMax) - } - - return nil -} - -// HttpcheckValidationPassedMetricAttributeKey specifies the key of an attribute for the httpcheck.validation.passed metric. -type HttpcheckValidationPassedMetricAttributeKey string - -const ( - HttpcheckValidationPassedMetricAttributeKeyHTTPURL HttpcheckValidationPassedMetricAttributeKey = "http.url" - HttpcheckValidationPassedMetricAttributeKeyValidationType HttpcheckValidationPassedMetricAttributeKey = "validation.type" -) - -// HttpcheckValidationPassedMetricConfig provides config for the httpcheck.validation.passed metric. -type HttpcheckValidationPassedMetricConfig struct { - Enabled bool `mapstructure:"enabled"` - enabledSetByUser bool - - AggregationStrategy string `mapstructure:"aggregation_strategy"` - EnabledAttributes []HttpcheckValidationPassedMetricAttributeKey `mapstructure:"attributes"` + AggregationStrategy string `mapstructure:"aggregation_strategy"` + EnabledAttributes []HttpcheckValidationOutcomeMetricAttributeKey `mapstructure:"attributes"` } -func (ms *HttpcheckValidationPassedMetricConfig) Unmarshal(parser *confmap.Conf) error { +func (ms *HttpcheckValidationOutcomeMetricConfig) Unmarshal(parser *confmap.Conf) error { if parser == nil { return nil } @@ -576,12 +529,12 @@ func (ms *HttpcheckValidationPassedMetricConfig) Unmarshal(parser *confmap.Conf) return nil } -func (ms *HttpcheckValidationPassedMetricConfig) Validate() error { +func (ms *HttpcheckValidationOutcomeMetricConfig) Validate() error { for _, val := range ms.EnabledAttributes { switch val { - case HttpcheckValidationPassedMetricAttributeKeyHTTPURL, HttpcheckValidationPassedMetricAttributeKeyValidationType: + case HttpcheckValidationOutcomeMetricAttributeKeyHTTPURL, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationType, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationTarget, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationOutcome: default: - return fmt.Errorf("metric httpcheck.validation.passed doesn't have an attribute %v, valid attributes: [http.url, validation.type]", val) + return fmt.Errorf("metric httpcheck.validation.outcome doesn't have an attribute %v, valid attributes: [http.url, httpcheck.validation.type, httpcheck.validation.target, httpcheck.validation.outcome]", val) } } @@ -606,8 +559,7 @@ type MetricsConfig struct { HttpcheckStatus HttpcheckStatusMetricConfig `mapstructure:"httpcheck.status"` HttpcheckTLSCertRemaining HttpcheckTLSCertRemainingMetricConfig `mapstructure:"httpcheck.tls.cert_remaining"` HttpcheckTLSHandshakeDuration HttpcheckTLSHandshakeDurationMetricConfig `mapstructure:"httpcheck.tls.handshake.duration"` - HttpcheckValidationFailed HttpcheckValidationFailedMetricConfig `mapstructure:"httpcheck.validation.failed"` - HttpcheckValidationPassed HttpcheckValidationPassedMetricConfig `mapstructure:"httpcheck.validation.passed"` + HttpcheckValidationOutcome HttpcheckValidationOutcomeMetricConfig `mapstructure:"httpcheck.validation.outcome"` } func DefaultMetricsConfig() MetricsConfig { @@ -662,15 +614,10 @@ func DefaultMetricsConfig() MetricsConfig { AggregationStrategy: AggregationStrategyAvg, EnabledAttributes: []HttpcheckTLSHandshakeDurationMetricAttributeKey{HttpcheckTLSHandshakeDurationMetricAttributeKeyHTTPURL}, }, - HttpcheckValidationFailed: HttpcheckValidationFailedMetricConfig{ - Enabled: false, - AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []HttpcheckValidationFailedMetricAttributeKey{HttpcheckValidationFailedMetricAttributeKeyHTTPURL, HttpcheckValidationFailedMetricAttributeKeyValidationType}, - }, - HttpcheckValidationPassed: HttpcheckValidationPassedMetricConfig{ + HttpcheckValidationOutcome: HttpcheckValidationOutcomeMetricConfig{ Enabled: false, AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []HttpcheckValidationPassedMetricAttributeKey{HttpcheckValidationPassedMetricAttributeKeyHTTPURL, HttpcheckValidationPassedMetricAttributeKeyValidationType}, + EnabledAttributes: []HttpcheckValidationOutcomeMetricAttributeKey{HttpcheckValidationOutcomeMetricAttributeKeyHTTPURL, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationType, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationTarget, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationOutcome}, }, } } diff --git a/receiver/httpcheckreceiver/internal/metadata/generated_config_test.go b/receiver/httpcheckreceiver/internal/metadata/generated_config_test.go index 1be45b8006c2d..023d95ed7388a 100644 --- a/receiver/httpcheckreceiver/internal/metadata/generated_config_test.go +++ b/receiver/httpcheckreceiver/internal/metadata/generated_config_test.go @@ -76,15 +76,10 @@ func TestMetricsBuilderConfig(t *testing.T) { AggregationStrategy: AggregationStrategyAvg, EnabledAttributes: []HttpcheckTLSHandshakeDurationMetricAttributeKey{HttpcheckTLSHandshakeDurationMetricAttributeKeyHTTPURL}, }, - HttpcheckValidationFailed: HttpcheckValidationFailedMetricConfig{ + HttpcheckValidationOutcome: HttpcheckValidationOutcomeMetricConfig{ Enabled: true, AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []HttpcheckValidationFailedMetricAttributeKey{HttpcheckValidationFailedMetricAttributeKeyHTTPURL, HttpcheckValidationFailedMetricAttributeKeyValidationType}, - }, - HttpcheckValidationPassed: HttpcheckValidationPassedMetricConfig{ - Enabled: true, - AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []HttpcheckValidationPassedMetricAttributeKey{HttpcheckValidationPassedMetricAttributeKeyHTTPURL, HttpcheckValidationPassedMetricAttributeKeyValidationType}, + EnabledAttributes: []HttpcheckValidationOutcomeMetricAttributeKey{HttpcheckValidationOutcomeMetricAttributeKeyHTTPURL, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationType, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationTarget, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationOutcome}, }, }, }, @@ -143,15 +138,10 @@ func TestMetricsBuilderConfig(t *testing.T) { AggregationStrategy: AggregationStrategyAvg, EnabledAttributes: []HttpcheckTLSHandshakeDurationMetricAttributeKey{HttpcheckTLSHandshakeDurationMetricAttributeKeyHTTPURL}, }, - HttpcheckValidationFailed: HttpcheckValidationFailedMetricConfig{ - Enabled: false, - AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []HttpcheckValidationFailedMetricAttributeKey{HttpcheckValidationFailedMetricAttributeKeyHTTPURL, HttpcheckValidationFailedMetricAttributeKeyValidationType}, - }, - HttpcheckValidationPassed: HttpcheckValidationPassedMetricConfig{ + HttpcheckValidationOutcome: HttpcheckValidationOutcomeMetricConfig{ Enabled: false, AggregationStrategy: AggregationStrategySum, - EnabledAttributes: []HttpcheckValidationPassedMetricAttributeKey{HttpcheckValidationPassedMetricAttributeKeyHTTPURL, HttpcheckValidationPassedMetricAttributeKeyValidationType}, + EnabledAttributes: []HttpcheckValidationOutcomeMetricAttributeKey{HttpcheckValidationOutcomeMetricAttributeKeyHTTPURL, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationType, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationTarget, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationOutcome}, }, }, }, @@ -160,7 +150,7 @@ func TestMetricsBuilderConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := loadMetricsBuilderConfig(t, tt.name) - diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(HttpcheckClientConnectionDurationMetricConfig{}, HttpcheckClientRequestDurationMetricConfig{}, HttpcheckDNSLookupDurationMetricConfig{}, HttpcheckDurationMetricConfig{}, HttpcheckErrorMetricConfig{}, HttpcheckResponseDurationMetricConfig{}, HttpcheckResponseSizeMetricConfig{}, HttpcheckStatusMetricConfig{}, HttpcheckTLSCertRemainingMetricConfig{}, HttpcheckTLSHandshakeDurationMetricConfig{}, HttpcheckValidationFailedMetricConfig{}, HttpcheckValidationPassedMetricConfig{})) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(HttpcheckClientConnectionDurationMetricConfig{}, HttpcheckClientRequestDurationMetricConfig{}, HttpcheckDNSLookupDurationMetricConfig{}, HttpcheckDurationMetricConfig{}, HttpcheckErrorMetricConfig{}, HttpcheckResponseDurationMetricConfig{}, HttpcheckResponseSizeMetricConfig{}, HttpcheckStatusMetricConfig{}, HttpcheckTLSCertRemainingMetricConfig{}, HttpcheckTLSHandshakeDurationMetricConfig{}, HttpcheckValidationOutcomeMetricConfig{})) require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) }) } diff --git a/receiver/httpcheckreceiver/internal/metadata/generated_metrics.go b/receiver/httpcheckreceiver/internal/metadata/generated_metrics.go index a7f53eb90eaba..36ae923b4f085 100644 --- a/receiver/httpcheckreceiver/internal/metadata/generated_metrics.go +++ b/receiver/httpcheckreceiver/internal/metadata/generated_metrics.go @@ -50,11 +50,8 @@ var MetricsInfo = metricsInfo{ HttpcheckTLSHandshakeDuration: metricInfo{ Name: "httpcheck.tls.handshake.duration", }, - HttpcheckValidationFailed: metricInfo{ - Name: "httpcheck.validation.failed", - }, - HttpcheckValidationPassed: metricInfo{ - Name: "httpcheck.validation.passed", + HttpcheckValidationOutcome: metricInfo{ + Name: "httpcheck.validation.outcome", }, } @@ -69,8 +66,7 @@ type metricsInfo struct { HttpcheckStatus metricInfo HttpcheckTLSCertRemaining metricInfo HttpcheckTLSHandshakeDuration metricInfo - HttpcheckValidationFailed metricInfo - HttpcheckValidationPassed metricInfo + HttpcheckValidationOutcome metricInfo } type metricInfo struct { @@ -995,17 +991,17 @@ func newMetricHttpcheckTLSHandshakeDuration(cfg HttpcheckTLSHandshakeDurationMet return m } -type metricHttpcheckValidationFailed struct { - data pmetric.Metric // data buffer for generated metric. - config HttpcheckValidationFailedMetricConfig // 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 +type metricHttpcheckValidationOutcome struct { + data pmetric.Metric // data buffer for generated metric. + config HttpcheckValidationOutcomeMetricConfig // 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 httpcheck.validation.failed metric with initial data. -func (m *metricHttpcheckValidationFailed) init() { - m.data.SetName("httpcheck.validation.failed") - m.data.SetDescription("Number of response validations that failed.") +// init fills httpcheck.validation.outcome metric with initial data. +func (m *metricHttpcheckValidationOutcome) init() { + m.data.SetName("httpcheck.validation.outcome") + m.data.SetDescription("Result of a response validation (1 for each validation, with outcome attribute).") m.data.SetUnit("{validation}") m.data.SetEmptySum() m.data.Sum().SetIsMonotonic(false) @@ -1014,7 +1010,7 @@ func (m *metricHttpcheckValidationFailed) init() { m.aggDataPoints = m.aggDataPoints[:0] } -func (m *metricHttpcheckValidationFailed) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, httpURLAttributeValue string, validationTypeAttributeValue string) { +func (m *metricHttpcheckValidationOutcome) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, httpURLAttributeValue string, httpcheckValidationTypeAttributeValue string, httpcheckValidationTargetAttributeValue string, httpcheckValidationOutcomeAttributeValue string) { if !m.config.Enabled { return } @@ -1022,105 +1018,17 @@ func (m *metricHttpcheckValidationFailed) recordDataPoint(start pcommon.Timestam dp := pmetric.NewNumberDataPoint() dp.SetStartTimestamp(start) dp.SetTimestamp(ts) - if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationFailedMetricAttributeKeyHTTPURL) { + if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationOutcomeMetricAttributeKeyHTTPURL) { dp.Attributes().PutStr("http.url", httpURLAttributeValue) } - if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationFailedMetricAttributeKeyValidationType) { - dp.Attributes().PutStr("validation.type", validationTypeAttributeValue) + if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationType) { + dp.Attributes().PutStr("httpcheck.validation.type", httpcheckValidationTypeAttributeValue) } - - 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 - } - } + if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationTarget) { + dp.Attributes().PutStr("httpcheck.validation.target", httpcheckValidationTargetAttributeValue) } - - 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 *metricHttpcheckValidationFailed) 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 *metricHttpcheckValidationFailed) 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 newMetricHttpcheckValidationFailed(cfg HttpcheckValidationFailedMetricConfig) metricHttpcheckValidationFailed { - m := metricHttpcheckValidationFailed{config: cfg} - - if cfg.Enabled { - m.data = pmetric.NewMetric() - m.init() - } - return m -} - -type metricHttpcheckValidationPassed struct { - data pmetric.Metric // data buffer for generated metric. - config HttpcheckValidationPassedMetricConfig // 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 httpcheck.validation.passed metric with initial data. -func (m *metricHttpcheckValidationPassed) init() { - m.data.SetName("httpcheck.validation.passed") - m.data.SetDescription("Number of response validations that passed.") - m.data.SetUnit("{validation}") - m.data.SetEmptySum() - m.data.Sum().SetIsMonotonic(false) - m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) - m.data.Sum().DataPoints().EnsureCapacity(m.capacity) - m.aggDataPoints = m.aggDataPoints[:0] -} - -func (m *metricHttpcheckValidationPassed) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, httpURLAttributeValue string, validationTypeAttributeValue string) { - if !m.config.Enabled { - return - } - - dp := pmetric.NewNumberDataPoint() - dp.SetStartTimestamp(start) - dp.SetTimestamp(ts) - if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationPassedMetricAttributeKeyHTTPURL) { - dp.Attributes().PutStr("http.url", httpURLAttributeValue) - } - if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationPassedMetricAttributeKeyValidationType) { - dp.Attributes().PutStr("validation.type", validationTypeAttributeValue) + if slices.Contains(m.config.EnabledAttributes, HttpcheckValidationOutcomeMetricAttributeKeyHttpcheckValidationOutcome) { + dp.Attributes().PutStr("httpcheck.validation.outcome", httpcheckValidationOutcomeAttributeValue) } var s string @@ -1153,14 +1061,14 @@ func (m *metricHttpcheckValidationPassed) recordDataPoint(start pcommon.Timestam } // updateCapacity saves max length of data point slices that will be used for the slice capacity. -func (m *metricHttpcheckValidationPassed) updateCapacity() { +func (m *metricHttpcheckValidationOutcome) 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 *metricHttpcheckValidationPassed) emit(metrics pmetric.MetricSlice) { +func (m *metricHttpcheckValidationOutcome) 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 { @@ -1173,8 +1081,8 @@ func (m *metricHttpcheckValidationPassed) emit(metrics pmetric.MetricSlice) { } } -func newMetricHttpcheckValidationPassed(cfg HttpcheckValidationPassedMetricConfig) metricHttpcheckValidationPassed { - m := metricHttpcheckValidationPassed{config: cfg} +func newMetricHttpcheckValidationOutcome(cfg HttpcheckValidationOutcomeMetricConfig) metricHttpcheckValidationOutcome { + m := metricHttpcheckValidationOutcome{config: cfg} if cfg.Enabled { m.data = pmetric.NewMetric() @@ -1201,8 +1109,7 @@ type MetricsBuilder struct { metricHttpcheckStatus metricHttpcheckStatus metricHttpcheckTLSCertRemaining metricHttpcheckTLSCertRemaining metricHttpcheckTLSHandshakeDuration metricHttpcheckTLSHandshakeDuration - metricHttpcheckValidationFailed metricHttpcheckValidationFailed - metricHttpcheckValidationPassed metricHttpcheckValidationPassed + metricHttpcheckValidationOutcome metricHttpcheckValidationOutcome } // MetricBuilderOption applies changes to default metrics builder. @@ -1238,8 +1145,7 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, opt metricHttpcheckStatus: newMetricHttpcheckStatus(mbc.Metrics.HttpcheckStatus), metricHttpcheckTLSCertRemaining: newMetricHttpcheckTLSCertRemaining(mbc.Metrics.HttpcheckTLSCertRemaining), metricHttpcheckTLSHandshakeDuration: newMetricHttpcheckTLSHandshakeDuration(mbc.Metrics.HttpcheckTLSHandshakeDuration), - metricHttpcheckValidationFailed: newMetricHttpcheckValidationFailed(mbc.Metrics.HttpcheckValidationFailed), - metricHttpcheckValidationPassed: newMetricHttpcheckValidationPassed(mbc.Metrics.HttpcheckValidationPassed), + metricHttpcheckValidationOutcome: newMetricHttpcheckValidationOutcome(mbc.Metrics.HttpcheckValidationOutcome), } for _, op := range options { @@ -1315,8 +1221,7 @@ func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { mb.metricHttpcheckStatus.emit(ils.Metrics()) mb.metricHttpcheckTLSCertRemaining.emit(ils.Metrics()) mb.metricHttpcheckTLSHandshakeDuration.emit(ils.Metrics()) - mb.metricHttpcheckValidationFailed.emit(ils.Metrics()) - mb.metricHttpcheckValidationPassed.emit(ils.Metrics()) + mb.metricHttpcheckValidationOutcome.emit(ils.Metrics()) for _, op := range options { op.apply(rm) @@ -1388,14 +1293,9 @@ func (mb *MetricsBuilder) RecordHttpcheckTLSHandshakeDurationDataPoint(ts pcommo mb.metricHttpcheckTLSHandshakeDuration.recordDataPoint(mb.startTime, ts, val, httpURLAttributeValue) } -// RecordHttpcheckValidationFailedDataPoint adds a data point to httpcheck.validation.failed metric. -func (mb *MetricsBuilder) RecordHttpcheckValidationFailedDataPoint(ts pcommon.Timestamp, val int64, httpURLAttributeValue string, validationTypeAttributeValue string) { - mb.metricHttpcheckValidationFailed.recordDataPoint(mb.startTime, ts, val, httpURLAttributeValue, validationTypeAttributeValue) -} - -// RecordHttpcheckValidationPassedDataPoint adds a data point to httpcheck.validation.passed metric. -func (mb *MetricsBuilder) RecordHttpcheckValidationPassedDataPoint(ts pcommon.Timestamp, val int64, httpURLAttributeValue string, validationTypeAttributeValue string) { - mb.metricHttpcheckValidationPassed.recordDataPoint(mb.startTime, ts, val, httpURLAttributeValue, validationTypeAttributeValue) +// RecordHttpcheckValidationOutcomeDataPoint adds a data point to httpcheck.validation.outcome metric. +func (mb *MetricsBuilder) RecordHttpcheckValidationOutcomeDataPoint(ts pcommon.Timestamp, val int64, httpURLAttributeValue string, httpcheckValidationTypeAttributeValue string, httpcheckValidationTargetAttributeValue string, httpcheckValidationOutcomeAttributeValue string) { + mb.metricHttpcheckValidationOutcome.recordDataPoint(mb.startTime, ts, val, httpURLAttributeValue, httpcheckValidationTypeAttributeValue, httpcheckValidationTargetAttributeValue, httpcheckValidationOutcomeAttributeValue) } // Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, diff --git a/receiver/httpcheckreceiver/internal/metadata/generated_metrics_test.go b/receiver/httpcheckreceiver/internal/metadata/generated_metrics_test.go index 7cc72566a0715..e66979bd072d1 100644 --- a/receiver/httpcheckreceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/httpcheckreceiver/internal/metadata/generated_metrics_test.go @@ -68,8 +68,7 @@ func TestMetricsBuilder(t *testing.T) { aggMap["httpcheck.status"] = mb.metricHttpcheckStatus.config.AggregationStrategy aggMap["httpcheck.tls.cert_remaining"] = mb.metricHttpcheckTLSCertRemaining.config.AggregationStrategy aggMap["httpcheck.tls.handshake.duration"] = mb.metricHttpcheckTLSHandshakeDuration.config.AggregationStrategy - aggMap["httpcheck.validation.failed"] = mb.metricHttpcheckValidationFailed.config.AggregationStrategy - aggMap["httpcheck.validation.passed"] = mb.metricHttpcheckValidationPassed.config.AggregationStrategy + aggMap["httpcheck.validation.outcome"] = mb.metricHttpcheckValidationOutcome.config.AggregationStrategy expectedWarnings := 0 if tt.metricsSet != testDataSetReag { @@ -143,15 +142,9 @@ func TestMetricsBuilder(t *testing.T) { } allMetricsCount++ - mb.RecordHttpcheckValidationFailedDataPoint(ts, 1, "http.url-val", "validation.type-val") + mb.RecordHttpcheckValidationOutcomeDataPoint(ts, 1, "http.url-val", "httpcheck.validation.type-val", "httpcheck.validation.target-val", "httpcheck.validation.outcome-val") if tt.name == "reaggregate_set" { - mb.RecordHttpcheckValidationFailedDataPoint(ts, 3, "http.url-val-2", "validation.type-val-2") - } - - allMetricsCount++ - mb.RecordHttpcheckValidationPassedDataPoint(ts, 1, "http.url-val", "validation.type-val") - if tt.name == "reaggregate_set" { - mb.RecordHttpcheckValidationPassedDataPoint(ts, 3, "http.url-val-2", "validation.type-val-2") + mb.RecordHttpcheckValidationOutcomeDataPoint(ts, 3, "http.url-val-2", "httpcheck.validation.type-val-2", "httpcheck.validation.target-val-2", "httpcheck.validation.outcome-val-2") } res := pcommon.NewResource() @@ -167,8 +160,7 @@ func TestMetricsBuilder(t *testing.T) { assert.Empty(t, mb.metricHttpcheckStatus.aggDataPoints) assert.Empty(t, mb.metricHttpcheckTLSCertRemaining.aggDataPoints) assert.Empty(t, mb.metricHttpcheckTLSHandshakeDuration.aggDataPoints) - assert.Empty(t, mb.metricHttpcheckValidationFailed.aggDataPoints) - assert.Empty(t, mb.metricHttpcheckValidationPassed.aggDataPoints) + assert.Empty(t, mb.metricHttpcheckValidationOutcome.aggDataPoints) } if tt.expectEmpty { @@ -644,13 +636,13 @@ func TestMetricsBuilder(t *testing.T) { _, ok := dp.Attributes().Get("http.url") assert.False(t, ok) } - case "httpcheck.validation.failed": + case "httpcheck.validation.outcome": if tt.name != "reaggregate_set" { - assert.False(t, validatedMetrics["httpcheck.validation.failed"], "Found a duplicate in the metrics slice: httpcheck.validation.failed") - validatedMetrics["httpcheck.validation.failed"] = true + assert.False(t, validatedMetrics["httpcheck.validation.outcome"], "Found a duplicate in the metrics slice: httpcheck.validation.outcome") + validatedMetrics["httpcheck.validation.outcome"] = true assert.Equal(t, pmetric.MetricTypeSum, mi.Type()) assert.Equal(t, 1, mi.Sum().DataPoints().Len()) - assert.Equal(t, "Number of response validations that failed.", mi.Description()) + assert.Equal(t, "Result of a response validation (1 for each validation, with outcome attribute).", mi.Description()) assert.Equal(t, "{validation}", mi.Unit()) assert.False(t, mi.Sum().IsMonotonic()) assert.Equal(t, pmetric.AggregationTemporalityCumulative, mi.Sum().AggregationTemporality()) @@ -662,64 +654,21 @@ func TestMetricsBuilder(t *testing.T) { httpURLAttrVal, ok := dp.Attributes().Get("http.url") assert.True(t, ok) assert.Equal(t, "http.url-val", httpURLAttrVal.Str()) - validationTypeAttrVal, ok := dp.Attributes().Get("validation.type") + httpcheckValidationTypeAttrVal, ok := dp.Attributes().Get("httpcheck.validation.type") assert.True(t, ok) - assert.Equal(t, "validation.type-val", validationTypeAttrVal.Str()) - } else { - assert.False(t, validatedMetrics["httpcheck.validation.failed"], "Found a duplicate in the metrics slice: httpcheck.validation.failed") - validatedMetrics["httpcheck.validation.failed"] = true - assert.Equal(t, pmetric.MetricTypeSum, mi.Type()) - assert.Equal(t, 1, mi.Sum().DataPoints().Len()) - assert.Equal(t, "Number of response validations that failed.", mi.Description()) - assert.Equal(t, "{validation}", mi.Unit()) - assert.False(t, mi.Sum().IsMonotonic()) - assert.Equal(t, pmetric.AggregationTemporalityCumulative, mi.Sum().AggregationTemporality()) - dp := mi.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["httpcheck.validation.failed"] { - 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("http.url") - assert.False(t, ok) - _, ok = dp.Attributes().Get("validation.type") - assert.False(t, ok) - } - case "httpcheck.validation.passed": - if tt.name != "reaggregate_set" { - assert.False(t, validatedMetrics["httpcheck.validation.passed"], "Found a duplicate in the metrics slice: httpcheck.validation.passed") - validatedMetrics["httpcheck.validation.passed"] = true - assert.Equal(t, pmetric.MetricTypeSum, mi.Type()) - assert.Equal(t, 1, mi.Sum().DataPoints().Len()) - assert.Equal(t, "Number of response validations that passed.", mi.Description()) - assert.Equal(t, "{validation}", mi.Unit()) - assert.False(t, mi.Sum().IsMonotonic()) - assert.Equal(t, pmetric.AggregationTemporalityCumulative, mi.Sum().AggregationTemporality()) - dp := mi.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()) - httpURLAttrVal, ok := dp.Attributes().Get("http.url") + assert.Equal(t, "httpcheck.validation.type-val", httpcheckValidationTypeAttrVal.Str()) + httpcheckValidationTargetAttrVal, ok := dp.Attributes().Get("httpcheck.validation.target") assert.True(t, ok) - assert.Equal(t, "http.url-val", httpURLAttrVal.Str()) - validationTypeAttrVal, ok := dp.Attributes().Get("validation.type") + assert.Equal(t, "httpcheck.validation.target-val", httpcheckValidationTargetAttrVal.Str()) + httpcheckValidationOutcomeAttrVal, ok := dp.Attributes().Get("httpcheck.validation.outcome") assert.True(t, ok) - assert.Equal(t, "validation.type-val", validationTypeAttrVal.Str()) + assert.Equal(t, "httpcheck.validation.outcome-val", httpcheckValidationOutcomeAttrVal.Str()) } else { - assert.False(t, validatedMetrics["httpcheck.validation.passed"], "Found a duplicate in the metrics slice: httpcheck.validation.passed") - validatedMetrics["httpcheck.validation.passed"] = true + assert.False(t, validatedMetrics["httpcheck.validation.outcome"], "Found a duplicate in the metrics slice: httpcheck.validation.outcome") + validatedMetrics["httpcheck.validation.outcome"] = true assert.Equal(t, pmetric.MetricTypeSum, mi.Type()) assert.Equal(t, 1, mi.Sum().DataPoints().Len()) - assert.Equal(t, "Number of response validations that passed.", mi.Description()) + assert.Equal(t, "Result of a response validation (1 for each validation, with outcome attribute).", mi.Description()) assert.Equal(t, "{validation}", mi.Unit()) assert.False(t, mi.Sum().IsMonotonic()) assert.Equal(t, pmetric.AggregationTemporalityCumulative, mi.Sum().AggregationTemporality()) @@ -727,7 +676,7 @@ func TestMetricsBuilder(t *testing.T) { assert.Equal(t, start, dp.StartTimestamp()) assert.Equal(t, ts, dp.Timestamp()) assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) - switch aggMap["httpcheck.validation.passed"] { + switch aggMap["httpcheck.validation.outcome"] { case "sum": assert.Equal(t, int64(4), dp.IntValue()) case "avg": @@ -739,7 +688,11 @@ func TestMetricsBuilder(t *testing.T) { } _, ok := dp.Attributes().Get("http.url") assert.False(t, ok) - _, ok = dp.Attributes().Get("validation.type") + _, ok = dp.Attributes().Get("httpcheck.validation.type") + assert.False(t, ok) + _, ok = dp.Attributes().Get("httpcheck.validation.target") + assert.False(t, ok) + _, ok = dp.Attributes().Get("httpcheck.validation.outcome") assert.False(t, ok) } } diff --git a/receiver/httpcheckreceiver/internal/metadata/testdata/config.yaml b/receiver/httpcheckreceiver/internal/metadata/testdata/config.yaml index a9bd22bcfa83c..a0dd1a620838a 100644 --- a/receiver/httpcheckreceiver/internal/metadata/testdata/config.yaml +++ b/receiver/httpcheckreceiver/internal/metadata/testdata/config.yaml @@ -31,12 +31,9 @@ all_set: httpcheck.tls.handshake.duration: enabled: true attributes: ["http.url"] - httpcheck.validation.failed: + httpcheck.validation.outcome: enabled: true - attributes: ["http.url","validation.type"] - httpcheck.validation.passed: - enabled: true - attributes: ["http.url","validation.type"] + attributes: ["http.url","httpcheck.validation.type","httpcheck.validation.target","httpcheck.validation.outcome"] reaggregate_set: metrics: httpcheck.client.connection.duration: @@ -69,10 +66,7 @@ reaggregate_set: httpcheck.tls.handshake.duration: enabled: true attributes: [] - httpcheck.validation.failed: - enabled: true - attributes: [] - httpcheck.validation.passed: + httpcheck.validation.outcome: enabled: true attributes: [] none_set: @@ -107,9 +101,6 @@ none_set: httpcheck.tls.handshake.duration: enabled: false attributes: ["http.url"] - httpcheck.validation.failed: - enabled: false - attributes: ["http.url","validation.type"] - httpcheck.validation.passed: + httpcheck.validation.outcome: enabled: false - attributes: ["http.url","validation.type"] + attributes: ["http.url","httpcheck.validation.type","httpcheck.validation.target","httpcheck.validation.outcome"] diff --git a/receiver/httpcheckreceiver/metadata.yaml b/receiver/httpcheckreceiver/metadata.yaml index 1ef1fb50b5c65..44634de1888a1 100644 --- a/receiver/httpcheckreceiver/metadata.yaml +++ b/receiver/httpcheckreceiver/metadata.yaml @@ -53,12 +53,20 @@ attributes: description: Full HTTP request URL. type: string requirement_level: recommended - network.transport: - description: OSI transport layer or inter-process communication method. + httpcheck.validation.outcome: + description: The result of the validation (passed or failed) + type: string + requirement_level: recommended + httpcheck.validation.target: + description: The validation expression being evaluated (JSONPath, contains string, regex pattern, size constraint) - statically configured type: string requirement_level: recommended - validation.type: - description: Type of validation performed (contains, json_path, size, regex) + httpcheck.validation.type: + description: Type of validation performed (json_path, contains, regex, size, not_contains) + type: string + requirement_level: recommended + network.transport: + description: OSI transport layer or inter-process communication method. type: string requirement_level: recommended @@ -147,18 +155,8 @@ metrics: value_type: int unit: ns attributes: [http.url] - httpcheck.validation.failed: - description: Number of response validations that failed. - enabled: false - stability: development - sum: - value_type: int - aggregation_temporality: cumulative - monotonic: false - unit: "{validation}" - attributes: [http.url, validation.type] - httpcheck.validation.passed: - description: Number of response validations that passed. + httpcheck.validation.outcome: + description: Result of a response validation (1 for each validation, with outcome attribute). enabled: false stability: development sum: @@ -166,4 +164,4 @@ metrics: aggregation_temporality: cumulative monotonic: false unit: "{validation}" - attributes: [http.url, validation.type] + attributes: [http.url, httpcheck.validation.type, httpcheck.validation.target, httpcheck.validation.outcome] \ No newline at end of file diff --git a/receiver/httpcheckreceiver/scraper.go b/receiver/httpcheckreceiver/scraper.go index 905c68c2b71fb..eadf6d03c3af9 100644 --- a/receiver/httpcheckreceiver/scraper.go +++ b/receiver/httpcheckreceiver/scraper.go @@ -83,6 +83,12 @@ func (t *timingInfo) getDurations() (dnsNs, tcpNs, tlsNs, requestNs, responseNs return dnsNs, tcpNs, tlsNs, requestNs, responseNs } +type validationResult struct { + Type string + Target string + Outcome string +} + type httpcheckScraper struct { clients []*http.Client cfg *Config @@ -124,77 +130,142 @@ func extractTLSInfo(state *tls.ConnectionState) (issuer, commonName string, sans } // validateResponse performs response validation based on configured rules -func validateResponse(body []byte, validations []validationConfig) (passed, failed map[string]int) { - passed = make(map[string]int) - failed = make(map[string]int) +func validateResponse(body []byte, validations []validationConfig) (results []validationResult) { + results = []validationResult{} for _, validation := range validations { // String matching validations if validation.Contains != "" { if strings.Contains(string(body), validation.Contains) { - passed["contains"]++ + results = append(results, validationResult{ + Type: "contains", + Target: validation.Contains, + Outcome: "passed", + }) } else { - failed["contains"]++ + results = append(results, validationResult{ + Type: "contains", + Target: validation.Contains, + Outcome: "failed", + }) } + continue } if validation.NotContains != "" { if !strings.Contains(string(body), validation.NotContains) { - passed["not_contains"]++ + results = append(results, validationResult{ + Type: "not_contains", + Target: validation.NotContains, + Outcome: "passed", + }) } else { - failed["not_contains"]++ + results = append(results, validationResult{ + Type: "not_contains", + Target: validation.NotContains, + Outcome: "failed", + }) } + continue } // JSON path validations if validation.JSONPath != "" { result := gjson.GetBytes(body, validation.JSONPath) + target := validation.JSONPath + if !result.Exists() { - failed["json_path"]++ + results = append(results, validationResult{ + Type: "json_path", + Target: target, + Outcome: "failed", + }) continue } if validation.Equals != "" { - if result.String() == validation.Equals { - passed["json_path"]++ + resultStr := result.String() + if resultStr == validation.Equals { + results = append(results, validationResult{ + Type: "json_path", + Target: target, + Outcome: "passed", + }) } else { - failed["json_path"]++ + results = append(results, validationResult{ + Type: "json_path", + Target: target, + Outcome: "failed", + }) } } else { - // If no equals condition, just check existence - passed["json_path"]++ + // No equals condition, just existence is enough + results = append(results, validationResult{ + Type: "json_path", + Target: target, + Outcome: "passed", + }) } + continue } // Size validations bodySize := int64(len(body)) if validation.MaxSize != nil { if bodySize <= *validation.MaxSize { - passed["size"]++ + results = append(results, validationResult{ + Type: "size", + Target: "max_size", + Outcome: "passed", + }) } else { - failed["size"]++ + results = append(results, validationResult{ + Type: "size", + Target: "max_size", + Outcome: "failed", + }) } + continue } if validation.MinSize != nil { if bodySize >= *validation.MinSize { - passed["size"]++ + results = append(results, validationResult{ + Type: "size", + Target: "min_size", + Outcome: "passed", + }) } else { - failed["size"]++ + results = append(results, validationResult{ + Type: "size", + Target: "min_size", + Outcome: "failed", + }) } + continue } // Regex validations if validation.Regex != "" { - if matched, err := regexp.Match(validation.Regex, body); err == nil && matched { - passed["regex"]++ + matched, err := regexp.Match(validation.Regex, body) + if err == nil && matched { + results = append(results, validationResult{ + Type: "regex", + Target: validation.Regex, + Outcome: "passed", + }) } else { - failed["regex"]++ + results = append(results, validationResult{ + Type: "regex", + Target: validation.Regex, + Outcome: "failed", + }) } + continue } } - return passed, failed + return results } // start initializes the scraper by creating HTTP clients for each endpoint. @@ -380,15 +451,18 @@ func (h *httpcheckScraper) scrape(ctx context.Context) (pmetric.Metrics, error) // Perform response validation if configured if len(h.cfg.Targets[targetIndex].Validations) > 0 && len(responseBody) > 0 { - passed, failed := validateResponse(responseBody, h.cfg.Targets[targetIndex].Validations) - - // Record validation metrics - for validationType, count := range passed { - h.mb.RecordHttpcheckValidationPassedDataPoint(now, int64(count), endpoint, validationType) - } + results := validateResponse(responseBody, h.cfg.Targets[targetIndex].Validations) - for validationType, count := range failed { - h.mb.RecordHttpcheckValidationFailedDataPoint(now, int64(count), endpoint, validationType) + // Record new structured metric - one datapoint per validation + for _, result := range results { + h.mb.RecordHttpcheckValidationOutcomeDataPoint( + now, + 1, + endpoint, + result.Type, + result.Target, + result.Outcome, + ) } } diff --git a/receiver/httpcheckreceiver/scraper_test.go b/receiver/httpcheckreceiver/scraper_test.go index ce53d2d44d31f..148ef4166af05 100644 --- a/receiver/httpcheckreceiver/scraper_test.go +++ b/receiver/httpcheckreceiver/scraper_test.go @@ -505,7 +505,8 @@ func TestScraperMultipleTargets(t *testing.T) { expectedMetrics, err := golden.ReadMetrics(goldenPath) require.NoError(t, err) - require.NoError(t, pmetrictest.CompareMetrics(expectedMetrics, actualMetrics, + require.NoError(t, pmetrictest.CompareMetrics( + expectedMetrics, actualMetrics, pmetrictest.IgnoreMetricAttributeValue("http.url"), pmetrictest.IgnoreMetricValues("httpcheck.duration"), pmetrictest.IgnoreMetricDataPointsOrder(), @@ -890,21 +891,21 @@ func TestAutoContentTypeConfiguration(t *testing.T) { } } -func TestResponseValidation(t *testing.T) { - // Create a mock server that returns JSON +func TestValidationOutcomeMetric(t *testing.T) { + // Create a mock server that returns JSON with multiple system statuses server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`{"status": "ok", "count": 5, "message": "healthy"}`)) + + _, err := w.Write([]byte(`{"system_1": true, "system_2": true, "system_3": false, "status": "ok", "message": "healthy"}`)) assert.NoError(t, err) })) defer server.Close() cfg := createDefaultConfig().(*Config) - // Enable validation metrics - cfg.Metrics.HttpcheckValidationPassed.Enabled = true - cfg.Metrics.HttpcheckValidationFailed.Enabled = true - cfg.Metrics.HttpcheckResponseSize.Enabled = true + + // Enable the new structured validation metric + cfg.Metrics.HttpcheckValidationOutcome.Enabled = true cfg.Targets = []*targetConfig{ { @@ -913,21 +914,25 @@ func TestResponseValidation(t *testing.T) { }, Validations: []validationConfig{ { - Contains: "healthy", + JSONPath: "system_1", + Equals: "true", }, { - JSONPath: "$.status", - Equals: "ok", + JSONPath: "system_2", + Equals: "true", }, { - JSONPath: "$.count", - Equals: "5", + JSONPath: "system_3", + Equals: "true", }, { - MaxSize: func() *int64 { size := int64(100); return &size }(), + Contains: "healthy", }, { - NotContains: "error", + Regex: "^.*healthy.*$", + }, + { + MaxSize: func() *int64 { size := int64(1000); return &size }(), }, }, }, @@ -939,78 +944,69 @@ func TestResponseValidation(t *testing.T) { metrics, err := scraper.scrape(t.Context()) require.NoError(t, err) - // Check that we have metrics - require.Positive(t, metrics.ResourceMetrics().Len()) + require.NotZero(t, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) ilm := rm.ScopeMetrics().At(0) - // Verify validation metrics are present - foundMetrics := make(map[string]bool) + foundMetric := false + validationCount := 0 + passedCount := 0 + failedCount := 0 + for i := 0; i < ilm.Metrics().Len(); i++ { metric := ilm.Metrics().At(i) - foundMetrics[metric.Name()] = true - } - // Check that validation metrics are present - assert.True(t, foundMetrics["httpcheck.validation.passed"]) - assert.True(t, foundMetrics["httpcheck.response.size"]) -} + if metric.Name() == "httpcheck.validation.outcome" { + foundMetric = true + dps := metric.Sum().DataPoints() -func TestResponseValidationFailures(t *testing.T) { - // Create a mock server that returns JSON with some failing conditions - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`{"status": "error", "count": 3, "message": "unhealthy"}`)) - assert.NoError(t, err) - })) - defer server.Close() + for j := 0; j < dps.Len(); j++ { + dp := dps.At(j) + validationCount++ - cfg := createDefaultConfig().(*Config) - // Enable validation metrics - cfg.Metrics.HttpcheckValidationPassed.Enabled = true - cfg.Metrics.HttpcheckValidationFailed.Enabled = true + // Verify value is 1 + assert.Equal(t, int64(1), dp.IntValue()) - cfg.Targets = []*targetConfig{ - { - ClientConfig: confighttp.ClientConfig{ - Endpoint: server.URL, - }, - Validations: []validationConfig{ - { - Contains: "healthy", // This will fail - }, - { - JSONPath: "$.status", - Equals: "ok", // This will fail - }, - { - JSONPath: "$.count", - Equals: "3", // This will pass - }, - }, - }, - } + // Verify attributes exist + _, ok := dp.Attributes().Get("http.url") + assert.True(t, ok) - scraper := newScraper(cfg, receivertest.NewNopSettings(metadata.Type)) - require.NoError(t, scraper.start(t.Context(), componenttest.NewNopHost())) + typeAttr, ok := dp.Attributes().Get("httpcheck.validation.type") + assert.True(t, ok) - metrics, err := scraper.scrape(t.Context()) - require.NoError(t, err) + targetAttr, ok := dp.Attributes().Get("httpcheck.validation.target") + assert.True(t, ok) - // Check that we have metrics - require.Positive(t, metrics.ResourceMetrics().Len()) - rm := metrics.ResourceMetrics().At(0) - ilm := rm.ScopeMetrics().At(0) + outcomeAttr, ok := dp.Attributes().Get("httpcheck.validation.outcome") + assert.True(t, ok) - // Verify validation metrics are present - foundMetrics := make(map[string]bool) - for i := 0; i < ilm.Metrics().Len(); i++ { - metric := ilm.Metrics().At(i) - foundMetrics[metric.Name()] = true + // Count outcomes + if outcomeAttr.Str() == "passed" { + passedCount++ + } else { + failedCount++ + } + + // Verify targets + if typeAttr.Str() == "json_path" { + assert.Contains(t, []string{"system_1", "system_2", "system_3"}, targetAttr.Str()) + } + if typeAttr.Str() == "contains" { + assert.Equal(t, "healthy", targetAttr.Str()) + } + if typeAttr.Str() == "regex" { + assert.Equal(t, "^.*healthy.*$", targetAttr.Str()) + } + if typeAttr.Str() == "size" { + assert.Equal(t, "max_size", targetAttr.Str()) + } + } + } } - // Check that validation metrics are present - assert.True(t, foundMetrics["httpcheck.validation.passed"]) - assert.True(t, foundMetrics["httpcheck.validation.failed"]) + assert.True(t, foundMetric, "Should have httpcheck.validation.outcome metric") + assert.Equal(t, 6, validationCount, "Should have 6 validation data points") + assert.Equal(t, 5, passedCount, "Should have 5 passed validations") + assert.Equal(t, 1, failedCount, "Should have 1 failed validation (system_3)") }