diff --git a/apps/alerting/historian/kinds/manifest.cue b/apps/alerting/historian/kinds/manifest.cue index 6f2bfc21cb9..404199834fd 100644 --- a/apps/alerting/historian/kinds/manifest.cue +++ b/apps/alerting/historian/kinds/manifest.cue @@ -1,34 +1,20 @@ package kinds +import ( + "github.com/grafana/grafana/apps/alerting/historian/kinds/v0alpha1" +) + manifest: { appName: "alerting-historian" groupOverride: "historian.alerting.grafana.app" versions: { - "v0alpha1": v0alpha1 + "v0alpha1": { + kinds: [dummyv0alpha1] + routes: v0alpha1.routes + } } } -v0alpha1: { - kinds: [dummyv0alpha1] - - routes: { - namespaced: { - // This endpoint is an exact copy of the existing /history endpoint, - // with the exception that error responses will be Kubernetes-style, - // not Grafana-style. It will be replaced in the future with a better - // more schema-friendly API. - "/alertstate/history": { - "GET": { - response: { - body: [string]: _ - } - responseMetadata: typeMeta: false - } - } - } - } -} - dummyv0alpha1: { kind: "Dummy" schema: { @@ -37,4 +23,4 @@ dummyv0alpha1: { dummyField: int } } -} \ No newline at end of file +} diff --git a/apps/alerting/historian/kinds/v0alpha1/matcher.cue b/apps/alerting/historian/kinds/v0alpha1/matcher.cue new file mode 100644 index 00000000000..71791ea93d2 --- /dev/null +++ b/apps/alerting/historian/kinds/v0alpha1/matcher.cue @@ -0,0 +1,9 @@ +package v0alpha1 + +#Matcher: { + type: "=" | "!=" | "=~" | "!~" @cuetsy(kind="enum",memberNames="Equal|NotEqual|EqualRegex|NotEqualRegex") + label: string + value: string +} + +#Matchers: [...#Matcher] diff --git a/apps/alerting/historian/kinds/v0alpha1/notification.cue b/apps/alerting/historian/kinds/v0alpha1/notification.cue new file mode 100644 index 00000000000..82de5b20c3c --- /dev/null +++ b/apps/alerting/historian/kinds/v0alpha1/notification.cue @@ -0,0 +1,65 @@ +package v0alpha1 + +import ( + "time" +) + +#NotificationStatus: "firing" | "resolved" @cog(kind="enum",memberNames="Firing|Resolved") + +#NotificationOutcome: "success" | "error" @cog(kind="enum",memberNames="Success|Error") + +#NotificationQuery: { + // From is the starting timestamp for the query. + from?: time.Time + // To is the starting timestamp for the query. + to?: time.Time + // Limit is the maximum number of entries to return. + limit?: int64 + // Receiver optionally filters the entries by receiver title (contact point). + receiver?: string + // Status optionally filters the entries to only either firing or resolved. + status?: #NotificationStatus + // Outcome optionally filters the entries to only either successful or failed attempts. + outcome?: #NotificationOutcome + // RuleUID optionally filters the entries to a specific alert rule. + ruleUID?: string + // GroupLabels optionally filters the entries by matching group labels. + groupLabels?: #Matchers +} + +#NotificationQueryResult: { + entries: [...#NotificationEntry] +} + +#NotificationEntry: { + // Timestamp is the time at which the notification attempt completed. + timestamp: time.Time + // Receiver is the receiver (contact point) title. + receiver: string + // Status indicates if the notification contains one or more firing alerts. + status: #NotificationStatus + // Outcome indicaes if the notificaion attempt was successful or if it failed. + outcome: #NotificationOutcome + // GroupLabels are the labels uniquely identifying the alert group within a route. + groupLabels: [string]: string + // Alerts are the alerts grouped into the notification. + alerts: [...#NotificationEntryAlert] + // Retry indicates if the attempt was a retried attempt. + retry: bool + // Error is the message returned by the contact point if delivery failed. + error?: string + // Duration is the length of time the notification attempt took in nanoseconds. + duration: int + // PipelineTime is the time at which the flush began. + pipelineTime: time.Time + // GroupKey uniquely idenifies the dispatcher alert group. + groupKey: string +} + +#NotificationEntryAlert: { + status: string + labels: [string]: string + annotations: [string]: string + startsAt: time.Time + endsAt: time.Time +} diff --git a/apps/alerting/historian/kinds/v0alpha1/routes.cue b/apps/alerting/historian/kinds/v0alpha1/routes.cue new file mode 100644 index 00000000000..976f1a5efb0 --- /dev/null +++ b/apps/alerting/historian/kinds/v0alpha1/routes.cue @@ -0,0 +1,29 @@ +package v0alpha1 + +routes: { + namespaced: { + // This endpoint is an exact copy of the existing /history endpoint, + // with the exception that error responses will be Kubernetes-style, + // not Grafana-style. It will be replaced in the future with a better + // more schema-friendly API. + "/alertstate/history": { + "GET": { + response: { + body: [string]: _ + } + responseMetadata: typeMeta: false + } + } + + // Query notification history. + "/notification/query": { + "POST": { + request: { + body: #NotificationQuery + } + response: #NotificationQueryResult + responseMetadata: typeMeta: false + } + } + } +} \ No newline at end of file diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/createnotificationquery_request_body_types_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/createnotificationquery_request_body_types_gen.go new file mode 100644 index 00000000000..62227705d39 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/createnotificationquery_request_body_types_gen.go @@ -0,0 +1,67 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +type CreateNotificationqueryRequestNotificationStatus string + +const ( + CreateNotificationqueryRequestNotificationStatusFiring CreateNotificationqueryRequestNotificationStatus = "firing" + CreateNotificationqueryRequestNotificationStatusResolved CreateNotificationqueryRequestNotificationStatus = "resolved" +) + +type CreateNotificationqueryRequestNotificationOutcome string + +const ( + CreateNotificationqueryRequestNotificationOutcomeSuccess CreateNotificationqueryRequestNotificationOutcome = "success" + CreateNotificationqueryRequestNotificationOutcomeError CreateNotificationqueryRequestNotificationOutcome = "error" +) + +type CreateNotificationqueryRequestMatchers []CreateNotificationqueryRequestMatcher + +type CreateNotificationqueryRequestMatcher struct { + Type CreateNotificationqueryRequestMatcherType `json:"type"` + Label string `json:"label"` + Value string `json:"value"` +} + +// NewCreateNotificationqueryRequestMatcher creates a new CreateNotificationqueryRequestMatcher object. +func NewCreateNotificationqueryRequestMatcher() *CreateNotificationqueryRequestMatcher { + return &CreateNotificationqueryRequestMatcher{} +} + +type CreateNotificationqueryRequestBody struct { + // From is the starting timestamp for the query. + From *time.Time `json:"from,omitempty"` + // To is the starting timestamp for the query. + To *time.Time `json:"to,omitempty"` + // Limit is the maximum number of entries to return. + Limit *int64 `json:"limit,omitempty"` + // Receiver optionally filters the entries by receiver title (contact point). + Receiver *string `json:"receiver,omitempty"` + // Status optionally filters the entries to only either firing or resolved. + Status *CreateNotificationqueryRequestNotificationStatus `json:"status,omitempty"` + // Outcome optionally filters the entries to only either successful or failed attempts. + Outcome *CreateNotificationqueryRequestNotificationOutcome `json:"outcome,omitempty"` + // RuleUID optionally filters the entries to a specific alert rule. + RuleUID *string `json:"ruleUID,omitempty"` + // GroupLabels optionally filters the entries by matching group labels. + GroupLabels *CreateNotificationqueryRequestMatchers `json:"groupLabels,omitempty"` +} + +// NewCreateNotificationqueryRequestBody creates a new CreateNotificationqueryRequestBody object. +func NewCreateNotificationqueryRequestBody() *CreateNotificationqueryRequestBody { + return &CreateNotificationqueryRequestBody{} +} + +type CreateNotificationqueryRequestMatcherType string + +const ( + CreateNotificationqueryRequestMatcherTypeEqual CreateNotificationqueryRequestMatcherType = "=" + CreateNotificationqueryRequestMatcherTypeNotEqual CreateNotificationqueryRequestMatcherType = "!=" + CreateNotificationqueryRequestMatcherTypeEqualRegex CreateNotificationqueryRequestMatcherType = "=~" + CreateNotificationqueryRequestMatcherTypeNotEqualRegex CreateNotificationqueryRequestMatcherType = "!~" +) diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/createnotificationquery_response_types_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/createnotificationquery_response_types_gen.go new file mode 100644 index 00000000000..9633a8dcf0d --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/createnotificationquery_response_types_gen.go @@ -0,0 +1,86 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// +k8s:openapi-gen=true +type NotificationEntry struct { + // Timestamp is the time at which the notification attempt completed. + Timestamp time.Time `json:"timestamp"` + // Receiver is the receiver (contact point) title. + Receiver string `json:"receiver"` + // Status indicates if the notification contains one or more firing alerts. + Status NotificationStatus `json:"status"` + // Outcome indicaes if the notificaion attempt was successful or if it failed. + Outcome NotificationOutcome `json:"outcome"` + // GroupLabels are the labels uniquely identifying the alert group within a route. + GroupLabels map[string]string `json:"groupLabels"` + // Alerts are the alerts grouped into the notification. + Alerts []NotificationEntryAlert `json:"alerts"` + // Retry indicates if the attempt was a retried attempt. + Retry bool `json:"retry"` + // Error is the message returned by the contact point if delivery failed. + Error *string `json:"error,omitempty"` + // Duration is the length of time the notification attempt took in nanoseconds. + Duration int64 `json:"duration"` + // PipelineTime is the time at which the flush began. + PipelineTime time.Time `json:"pipelineTime"` + // GroupKey uniquely idenifies the dispatcher alert group. + GroupKey string `json:"groupKey"` +} + +// NewNotificationEntry creates a new NotificationEntry object. +func NewNotificationEntry() *NotificationEntry { + return &NotificationEntry{ + GroupLabels: map[string]string{}, + Alerts: []NotificationEntryAlert{}, + } +} + +// +k8s:openapi-gen=true +type NotificationStatus string + +const ( + NotificationStatusFiring NotificationStatus = "firing" + NotificationStatusResolved NotificationStatus = "resolved" +) + +// +k8s:openapi-gen=true +type NotificationOutcome string + +const ( + NotificationOutcomeSuccess NotificationOutcome = "success" + NotificationOutcomeError NotificationOutcome = "error" +) + +// +k8s:openapi-gen=true +type NotificationEntryAlert struct { + Status string `json:"status"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` +} + +// NewNotificationEntryAlert creates a new NotificationEntryAlert object. +func NewNotificationEntryAlert() *NotificationEntryAlert { + return &NotificationEntryAlert{ + Labels: map[string]string{}, + Annotations: map[string]string{}, + } +} + +// +k8s:openapi-gen=true +type CreateNotificationquery struct { + Entries []NotificationEntry `json:"entries"` +} + +// NewCreateNotificationquery creates a new CreateNotificationquery object. +func NewCreateNotificationquery() *CreateNotificationquery { + return &CreateNotificationquery{ + Entries: []NotificationEntry{}, + } +} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go b/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go index 4e6401e5b1d..6a353034afa 100644 --- a/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go +++ b/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go @@ -92,9 +92,321 @@ var appManifestData = app.ManifestData{ }, }, }, + "/notification/query": { + Post: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "createNotificationquery", + + RequestBody: &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "from": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + Description: "From is the starting timestamp for the query.", + }, + }, + "groupLabels": { + SchemaProps: spec.SchemaProps{ + + Description: "GroupLabels optionally filters the entries by matching group labels.", + Ref: spec.MustCreateRef("#/components/schemas/createNotificationqueryMatchers"), + }, + }, + "limit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Description: "Limit is the maximum number of entries to return.", + }, + }, + "outcome": { + SchemaProps: spec.SchemaProps{ + + Description: "Outcome optionally filters the entries to only either successful or failed attempts.", + Ref: spec.MustCreateRef("#/components/schemas/createNotificationqueryNotificationOutcome"), + }, + }, + "receiver": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Receiver optionally filters the entries by receiver title (contact point).", + }, + }, + "ruleUID": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "RuleUID optionally filters the entries to a specific alert rule.", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + + Description: "Status optionally filters the entries to only either firing or resolved.", + Ref: spec.MustCreateRef("#/components/schemas/createNotificationqueryNotificationStatus"), + }, + }, + "to": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + Description: "To is the starting timestamp for the query.", + }, + }, + }, + }}, + }}, + }, + }}, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "entries": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + }, + }, + }, + Required: []string{ + "entries", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, }, Cluster: map[string]spec3.PathProps{}, - Schemas: map[string]spec.Schema{}, + Schemas: map[string]spec.Schema{ + "createNotificationqueryMatcher": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Enum: []interface{}{ + "=", + "!=", + "=~", + "!~", + }, + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "type", + "label", + "value", + }, + }, + }, + "createNotificationqueryMatchers": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + }, + }, + "createNotificationqueryNotificationEntry": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "alerts": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Description: "Alerts are the alerts grouped into the notification.", + }, + }, + "duration": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Description: "Duration is the length of time the notification attempt took in nanoseconds.", + }, + }, + "error": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Error is the message returned by the contact point if delivery failed.", + }, + }, + "groupKey": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "GroupKey uniquely idenifies the dispatcher alert group.", + }, + }, + "groupLabels": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Description: "GroupLabels are the labels uniquely identifying the alert group within a route.", + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + "outcome": { + SchemaProps: spec.SchemaProps{ + + Description: "Outcome indicaes if the notificaion attempt was successful or if it failed.", + Ref: spec.MustCreateRef("#/components/schemas/createNotificationqueryNotificationOutcome"), + }, + }, + "pipelineTime": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + Description: "PipelineTime is the time at which the flush began.", + }, + }, + "receiver": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Receiver is the receiver (contact point) title.", + }, + }, + "retry": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Description: "Retry indicates if the attempt was a retried attempt.", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + + Description: "Status indicates if the notification contains one or more firing alerts.", + Ref: spec.MustCreateRef("#/components/schemas/createNotificationqueryNotificationStatus"), + }, + }, + "timestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + Description: "Timestamp is the time at which the notification attempt completed.", + }, + }, + }, + Required: []string{ + "timestamp", + "receiver", + "status", + "outcome", + "groupLabels", + "alerts", + "retry", + "duration", + "pipelineTime", + "groupKey", + }, + }, + }, + "createNotificationqueryNotificationEntryAlert": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "annotations": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + "endsAt": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "labels": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + "startsAt": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "status", + "labels", + "annotations", + "startsAt", + "endsAt", + }, + }, + }, + "createNotificationqueryNotificationOutcome": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Enum: []interface{}{ + "success", + "error", + }, + }, + }, + "createNotificationqueryNotificationStatus": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Enum: []interface{}{ + "firing", + "resolved", + }, + }, + }, + }, }, }, }, @@ -120,7 +432,8 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist } var customRouteToGoResponseType = map[string]any{ - "v0alpha1||/alertstate/history|GET": v0alpha1.GetAlertstatehistory{}, + "v0alpha1||/alertstate/history|GET": v0alpha1.GetAlertstatehistory{}, + "v0alpha1||/notification/query|POST": v0alpha1.CreateNotificationquery{}, } // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. @@ -145,7 +458,9 @@ func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goTyp return goType, exists } -var customRouteToGoRequestBodyType = map[string]any{} +var customRouteToGoRequestBodyType = map[string]any{ + "v0alpha1||/notification/query|POST": v0alpha1.CreateNotificationqueryRequestBody{}, +} func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { diff --git a/apps/alerting/historian/pkg/app/app.go b/apps/alerting/historian/pkg/app/app.go index 8d5feb39ec7..e34e03c9044 100644 --- a/apps/alerting/historian/pkg/app/app.go +++ b/apps/alerting/historian/pkg/app/app.go @@ -1,8 +1,13 @@ package app import ( + "context" + "net/http" + "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/simple" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/grafana/grafana/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1" "github.com/grafana/grafana/apps/alerting/historian/pkg/app/config" @@ -21,6 +26,11 @@ func New(cfg app.Config) (app.App, error) { Path: "/alertstate/history", Method: "GET", }: runtimeConfig.GetAlertStateHistoryHandler, + { + Namespaced: true, + Path: "/notification/query", + Method: "POST", + }: UnimplementedHandler, }, }, // TODO: Remove when SDK is fixed. @@ -43,3 +53,13 @@ func New(cfg app.Config) (app.App, error) { return a, nil } + +func UnimplementedHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusUnprocessableEntity, + Message: "unimplemented", + }, + } +}