diff --git a/manifest/v1alpha/agent/agent.go b/manifest/v1alpha/agent/agent.go index 138cca8d..ecc04568 100644 --- a/manifest/v1alpha/agent/agent.go +++ b/manifest/v1alpha/agent/agent.go @@ -68,6 +68,7 @@ type Spec struct { Generic *GenericConfig `json:"generic,omitempty"` Honeycomb *HoneycombConfig `json:"honeycomb,omitempty"` LogicMonitor *LogicMonitorConfig `json:"logicMonitor,omitempty"` + AzurePrometheus *AzurePrometheusConfig `json:"azurePrometheus,omitempty"` HistoricalDataRetrieval *v1alpha.HistoricalDataRetrieval `json:"historicalDataRetrieval,omitempty"` QueryDelay *v1alpha.QueryDelay `json:"queryDelay,omitempty"` // Interval, Timeout and Jitter are readonly and cannot be set via API @@ -139,6 +140,8 @@ func (spec Spec) GetType() (v1alpha.DataSourceType, error) { return v1alpha.Honeycomb, nil case spec.LogicMonitor != nil: return v1alpha.LogicMonitor, nil + case spec.AzurePrometheus != nil: + return v1alpha.AzurePrometheus, nil } return 0, errors.New("unknown agent type") } @@ -258,3 +261,9 @@ type HoneycombConfig struct{} type LogicMonitorConfig struct { Account string `json:"account"` } + +// AzurePrometheusConfig represents content of Azure Monitor managed service for Prometheus typical for Agent Object. +type AzurePrometheusConfig struct { + URL string `json:"url"` + TenantID string `json:"tenantId"` +} diff --git a/manifest/v1alpha/agent/validation.go b/manifest/v1alpha/agent/validation.go index d6c40d0e..c050e95b 100644 --- a/manifest/v1alpha/agent/validation.go +++ b/manifest/v1alpha/agent/validation.go @@ -124,6 +124,9 @@ var specValidation = validation.New[Spec]( validation.ForPointer(func(s Spec) *LogicMonitorConfig { return s.LogicMonitor }). WithName("logicMonitor"). Include(logicMonitorValidation), + validation.ForPointer(func(s Spec) *AzurePrometheusConfig { return s.AzurePrometheus }). + WithName("azurePrometheus"). + Include(azurePrometheusValidation), ) var ( @@ -201,6 +204,16 @@ var ( Required(). Rules(validation.StringNotEmpty()), ) + azurePrometheusValidation = validation.New[AzurePrometheusConfig]( + validation.For(func(a AzurePrometheusConfig) string { return a.URL }). + WithName("url"). + Required(). + Rules(validation.StringURL()), + validation.For(func(a AzurePrometheusConfig) string { return a.TenantID }). + WithName("tenantId"). + Required(). + Rules(validation.StringUUID()), + ) // URL only. prometheusValidation = newURLValidator(func(p PrometheusConfig) string { return p.URL }) appDynamicsValidation = newURLValidator(func(a AppDynamicsConfig) string { return a.URL }) @@ -371,6 +384,11 @@ var exactlyOneDataSourceTypeValidationRule = validation.NewSingleRule(func(spec return err } } + if spec.AzurePrometheus != nil { + if err := typesMatch(v1alpha.AzurePrometheus); err != nil { + return err + } + } if onlyType == 0 { return errors.New("must have exactly one data source type, none were provided") } diff --git a/manifest/v1alpha/agent/validation_test.go b/manifest/v1alpha/agent/validation_test.go index 76302f24..f5070aff 100644 --- a/manifest/v1alpha/agent/validation_test.go +++ b/manifest/v1alpha/agent/validation_test.go @@ -744,6 +744,72 @@ func TestValidateSpec_AzureMonitor(t *testing.T) { }) } +func TestValidateSpec_LogicMonitor(t *testing.T) { + t.Run("passes", func(t *testing.T) { + agent := validAgent(v1alpha.LogicMonitor) + err := validate(agent) + testutils.AssertNoError(t, agent, err) + }) + t.Run("required account", func(t *testing.T) { + agent := validAgent(v1alpha.LogicMonitor) + agent.Spec.LogicMonitor.Account = "" + err := validate(agent) + testutils.AssertContainsErrors(t, agent, err, 1, testutils.ExpectedError{ + Prop: "spec.logicMonitor.account", + Code: validation.ErrorCodeRequired, + }) + }) +} + +func TestValidateSpec_AzurePrometheus(t *testing.T) { + t.Run("passes", func(t *testing.T) { + agent := validAgent(v1alpha.AzurePrometheus) + err := validate(agent) + testutils.AssertNoError(t, agent, err) + }) + t.Run("invalid tenantId", func(t *testing.T) { + agent := validAgent(v1alpha.AzurePrometheus) + agent.Spec.AzurePrometheus.TenantID = "invalid" + err := validate(agent) + testutils.AssertContainsErrors(t, agent, err, 1, testutils.ExpectedError{ + Prop: "spec.azurePrometheus.tenantId", + Code: validation.ErrorCodeStringUUID, + }) + }) + t.Run("required fields", func(t *testing.T) { + agent := validAgent(v1alpha.AzurePrometheus) + agent.Spec.AzurePrometheus.URL = "" + agent.Spec.AzurePrometheus.TenantID = "" + err := validate(agent) + testutils.AssertContainsErrors(t, agent, err, 2, + testutils.ExpectedError{ + Prop: "spec.azurePrometheus.url", + Code: validation.ErrorCodeRequired, + }, + testutils.ExpectedError{ + Prop: "spec.azurePrometheus.tenantId", + Code: validation.ErrorCodeRequired, + }, + ) + }) + t.Run("invalid fields", func(t *testing.T) { + agent := validAgent(v1alpha.AzurePrometheus) + agent.Spec.AzurePrometheus.URL = "invalid" + agent.Spec.AzurePrometheus.TenantID = strings.Repeat("l", 256) + err := validate(agent) + testutils.AssertContainsErrors(t, agent, err, 2, + testutils.ExpectedError{ + Prop: "spec.azurePrometheus.url", + Code: validation.ErrorCodeStringURL, + }, + testutils.ExpectedError{ + Prop: "spec.azurePrometheus.tenantId", + Code: validation.ErrorCodeStringUUID, + }, + ) + }) +} + func validAgent(typ v1alpha.DataSourceType) Agent { spec := validAgentSpec(typ) spec.Description = fmt.Sprintf("Example %s Agent", typ) @@ -874,6 +940,12 @@ func validAgentSpec(typ v1alpha.DataSourceType) Spec { Account: "account", }, }, + v1alpha.AzurePrometheus: { + AzurePrometheus: &AzurePrometheusConfig{ + URL: "https://prometheus-service.monitoring:8080", + TenantID: "e190c630-8873-11ee-b9d1-0242ac120002", + }, + }, } return specs[typ] diff --git a/manifest/v1alpha/data_sources.go b/manifest/v1alpha/data_sources.go index b1ba4b18..30774d17 100644 --- a/manifest/v1alpha/data_sources.go +++ b/manifest/v1alpha/data_sources.go @@ -47,6 +47,7 @@ AzureMonitor Generic Honeycomb LogicMonitor +AzurePrometheus )*/ type DataSourceType int @@ -321,6 +322,7 @@ var agentDataRetrievalMaxDuration = map[DataSourceType]HistoricalRetrievalDurati AzureMonitor: {Value: ptr(30), Unit: HRDDay}, Honeycomb: {Value: ptr(7), Unit: HRDDay}, GoogleCloudMonitoring: {Value: ptr(30), Unit: HRDDay}, + AzurePrometheus: {Value: ptr(30), Unit: HRDDay}, } var directDataRetrievalMaxDuration = map[DataSourceType]HistoricalRetrievalDuration{ @@ -469,6 +471,10 @@ func GetQueryDelayDefaults() QueryDelayDefaults { Value: ptr(2), Unit: Minute, }, + AzurePrometheus: { + Value: ptr(0), + Unit: Second, + }, } } diff --git a/manifest/v1alpha/data_sources_enum.go b/manifest/v1alpha/data_sources_enum.go index 72e4207b..90c00505 100644 --- a/manifest/v1alpha/data_sources_enum.go +++ b/manifest/v1alpha/data_sources_enum.go @@ -65,11 +65,13 @@ const ( Honeycomb // LogicMonitor is a DataSourceType of type LogicMonitor. LogicMonitor + // AzurePrometheus is a DataSourceType of type AzurePrometheus. + AzurePrometheus ) var ErrInvalidDataSourceType = errors.New("not a valid DataSourceType") -const _DataSourceTypeName = "PrometheusDatadogNewRelicAppDynamicsSplunkLightstepSplunkObservabilityDynatraceThousandEyesGraphiteBigQueryElasticsearchOpenTSDBGrafanaLokiCloudWatchPingdomAmazonPrometheusRedshiftSumoLogicInstanaInfluxDBGoogleCloudMonitoringAzureMonitorGenericHoneycombLogicMonitor" +const _DataSourceTypeName = "PrometheusDatadogNewRelicAppDynamicsSplunkLightstepSplunkObservabilityDynatraceThousandEyesGraphiteBigQueryElasticsearchOpenTSDBGrafanaLokiCloudWatchPingdomAmazonPrometheusRedshiftSumoLogicInstanaInfluxDBGoogleCloudMonitoringAzureMonitorGenericHoneycombLogicMonitorAzurePrometheus" // DataSourceTypeValues returns a list of the values for DataSourceType func DataSourceTypeValues() []DataSourceType { @@ -100,6 +102,7 @@ func DataSourceTypeValues() []DataSourceType { Generic, Honeycomb, LogicMonitor, + AzurePrometheus, } } @@ -130,6 +133,7 @@ var _DataSourceTypeMap = map[DataSourceType]string{ Generic: _DataSourceTypeName[237:244], Honeycomb: _DataSourceTypeName[244:253], LogicMonitor: _DataSourceTypeName[253:265], + AzurePrometheus: _DataSourceTypeName[265:280], } // String implements the Stringer interface. @@ -174,6 +178,7 @@ var _DataSourceTypeValue = map[string]DataSourceType{ _DataSourceTypeName[237:244]: Generic, _DataSourceTypeName[244:253]: Honeycomb, _DataSourceTypeName[253:265]: LogicMonitor, + _DataSourceTypeName[265:280]: AzurePrometheus, } // ParseDataSourceType attempts to convert a string to a DataSourceType. diff --git a/manifest/v1alpha/slo/metrics.go b/manifest/v1alpha/slo/metrics.go index 6b00ebe1..dc103173 100644 --- a/manifest/v1alpha/slo/metrics.go +++ b/manifest/v1alpha/slo/metrics.go @@ -47,6 +47,7 @@ type MetricSpec struct { Generic *GenericMetric `json:"generic,omitempty"` Honeycomb *HoneycombMetric `json:"honeycomb,omitempty"` LogicMonitor *LogicMonitorMetric `json:"logicMonitor,omitempty"` + AzurePrometheus *AzurePrometheusMetric `json:"azurePrometheus,omitempty"` } func (s *Spec) containsIndicatorRawMetric() bool { @@ -265,6 +266,8 @@ func (m *MetricSpec) DataSourceType() v1alpha.DataSourceType { return v1alpha.Honeycomb case m.LogicMonitor != nil: return v1alpha.LogicMonitor + case m.AzurePrometheus != nil: + return v1alpha.AzurePrometheus default: return 0 } @@ -349,6 +352,8 @@ func (m *MetricSpec) Query() interface{} { return m.Honeycomb case v1alpha.LogicMonitor: return m.LogicMonitor + case v1alpha.AzurePrometheus: + return m.AzurePrometheus default: return nil } diff --git a/manifest/v1alpha/slo/metrics_azure_prometheus.go b/manifest/v1alpha/slo/metrics_azure_prometheus.go new file mode 100644 index 00000000..50584a4c --- /dev/null +++ b/manifest/v1alpha/slo/metrics_azure_prometheus.go @@ -0,0 +1,15 @@ +package slo + +import "github.com/nobl9/nobl9-go/internal/validation" + +// AzurePrometheusMetric represents metric from Azure Monitor managed service for Prometheus +type AzurePrometheusMetric struct { + PromQL string `json:"promql"` +} + +var azurePrometheusValidation = validation.New[AzurePrometheusMetric]( + validation.For(func(p AzurePrometheusMetric) string { return p.PromQL }). + WithName("promql"). + Required(). + Rules(validation.StringNotEmpty()), +) diff --git a/manifest/v1alpha/slo/metrics_azure_prometheus_test.go b/manifest/v1alpha/slo/metrics_azure_prometheus_test.go new file mode 100644 index 00000000..07e0336b --- /dev/null +++ b/manifest/v1alpha/slo/metrics_azure_prometheus_test.go @@ -0,0 +1,35 @@ +package slo + +import ( + "testing" + + "github.com/nobl9/nobl9-go/internal/testutils" + "github.com/nobl9/nobl9-go/internal/validation" + "github.com/nobl9/nobl9-go/manifest/v1alpha" +) + +func TestAzurePrometheus(t *testing.T) { + t.Run("passes", func(t *testing.T) { + slo := validRawMetricSLO(v1alpha.AzurePrometheus) + err := validate(slo) + testutils.AssertNoError(t, slo, err) + }) + t.Run("required", func(t *testing.T) { + slo := validRawMetricSLO(v1alpha.AzurePrometheus) + slo.Spec.Objectives[0].RawMetric.MetricQuery.AzurePrometheus = &AzurePrometheusMetric{} + err := validate(slo) + testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{ + Prop: "spec.objectives[0].rawMetric.query.azurePrometheus.promql", + Code: validation.ErrorCodeRequired, + }) + }) + t.Run("empty", func(t *testing.T) { + slo := validRawMetricSLO(v1alpha.AzurePrometheus) + slo.Spec.Objectives[0].RawMetric.MetricQuery.AzurePrometheus.PromQL = "" + err := validate(slo) + testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{ + Prop: "spec.objectives[0].rawMetric.query.azurePrometheus.promql", + Code: validation.ErrorCodeRequired, + }) + }) +} diff --git a/manifest/v1alpha/slo/metrics_logic_monitor_test.go b/manifest/v1alpha/slo/metrics_logic_monitor_test.go index 359d8be0..48b3ae48 100644 --- a/manifest/v1alpha/slo/metrics_logic_monitor_test.go +++ b/manifest/v1alpha/slo/metrics_logic_monitor_test.go @@ -15,9 +15,35 @@ func TestLogicMonitor(t *testing.T) { testutils.AssertNoError(t, slo, err) }) t.Run("required", func(t *testing.T) { + slo := validRawMetricSLO(v1alpha.LogicMonitor) + slo.Spec.Objectives[0].RawMetric.MetricQuery.LogicMonitor = &LogicMonitorMetric{} + err := validate(slo) + testutils.AssertContainsErrors(t, slo, err, 4, + testutils.ExpectedError{ + Prop: "spec.objectives[0].rawMetric.query.logicMonitor.queryType", + Code: validation.ErrorCodeRequired, + }, + testutils.ExpectedError{ + Prop: "spec.objectives[0].rawMetric.query.logicMonitor.deviceDataSourceInstanceId", + Code: validation.ErrorCodeRequired, + }, + testutils.ExpectedError{ + Prop: "spec.objectives[0].rawMetric.query.logicMonitor.graphId", + Code: validation.ErrorCodeRequired, + }, + testutils.ExpectedError{ + Prop: "spec.objectives[0].rawMetric.query.logicMonitor.line", + Code: validation.ErrorCodeRequired, + }, + ) + }) + t.Run("invalid fields", func(t *testing.T) { slo := validRawMetricSLO(v1alpha.LogicMonitor) slo.Spec.Objectives[0].RawMetric.MetricQuery.LogicMonitor = &LogicMonitorMetric{ - QueryType: "wrongQueryType", + QueryType: "wrong-type", + DeviceDataSourceInstanceID: -1, + GraphID: -1, + Line: "", } err := validate(slo) testutils.AssertContainsErrors(t, slo, err, 4, @@ -27,11 +53,11 @@ func TestLogicMonitor(t *testing.T) { }, testutils.ExpectedError{ Prop: "spec.objectives[0].rawMetric.query.logicMonitor.deviceDataSourceInstanceId", - Code: validation.ErrorCodeRequired, + Code: validation.ErrorCodeGreaterThanOrEqualTo, }, testutils.ExpectedError{ Prop: "spec.objectives[0].rawMetric.query.logicMonitor.graphId", - Code: validation.ErrorCodeRequired, + Code: validation.ErrorCodeGreaterThanOrEqualTo, }, testutils.ExpectedError{ Prop: "spec.objectives[0].rawMetric.query.logicMonitor.line", diff --git a/manifest/v1alpha/slo/metrics_validation.go b/manifest/v1alpha/slo/metrics_validation.go index a61334be..cabbdb03 100644 --- a/manifest/v1alpha/slo/metrics_validation.go +++ b/manifest/v1alpha/slo/metrics_validation.go @@ -179,6 +179,9 @@ var metricSpecValidation = validation.New[MetricSpec]( validation.ForPointer(func(m MetricSpec) *LogicMonitorMetric { return m.LogicMonitor }). WithName("logicMonitor"). Include(logicMonitorValidation), + validation.ForPointer(func(m MetricSpec) *AzurePrometheusMetric { return m.AzurePrometheus }). + WithName("azurePrometheus"). + Include(azurePrometheusValidation), ) var badOverTotalEnabledSources = []v1alpha.DataSourceType{ @@ -187,6 +190,7 @@ var badOverTotalEnabledSources = []v1alpha.DataSourceType{ v1alpha.AzureMonitor, v1alpha.Honeycomb, v1alpha.LogicMonitor, + v1alpha.AzurePrometheus, } // Support for bad/total metrics will be enabled gradually. @@ -353,6 +357,11 @@ func validateExactlyOneMetricSpecType(metrics ...*MetricSpec) error { return err } } + if metric.AzurePrometheus != nil { + if err := typesMatch(v1alpha.AzurePrometheus); err != nil { + return err + } + } } if onlyType == 0 { return errors.New("must have exactly one metric spec type, none were provided") diff --git a/manifest/v1alpha/slo/validation_test.go b/manifest/v1alpha/slo/validation_test.go index 889ff618..6cc52f74 100644 --- a/manifest/v1alpha/slo/validation_test.go +++ b/manifest/v1alpha/slo/validation_test.go @@ -1677,6 +1677,9 @@ fetch consumed_api GraphID: 11354, Line: "MAXRTT", }}, + v1alpha.AzurePrometheus: {AzurePrometheus: &AzurePrometheusMetric{ + PromQL: "sum(rate(prometheus_http_requests_total[1h]))", + }}, } func ptr[T any](v T) *T { return &v }