Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions pkg/handler/alertingmock/alerting_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package alertingmock

import (
"encoding/json"
"fmt"
"math/rand/v2"
"net/http"
"net/url"
"strings"

"github.com/prometheus/common/model"
"github.com/sirupsen/logrus"
)

var mlog = logrus.WithField("module", "alertingmock")

var namespaces = []string{"uss-enterprise", "uss-raven", "la-sirena", "sh-raan", "the-whale-probe", "uss-defiant", "scimitar", "romulan-warbird", "uss-excelsior", "galileo", "phoenix"}
var nodes = []string{"caldonia", "denobula", "vulcan"}

// We duplicate prom model due to missing json information
type AlertingRule struct {
Name string `json:"name"`
Labels model.LabelSet `json:"labels"`
Annotations model.LabelSet `json:"annotations"`
Alerts []*Alert `json:"alerts"`
State string `json:"state"`
}

type Alert struct {
Annotations model.LabelSet `json:"annotations"`
Labels model.LabelSet `json:"labels"`
State string `json:"state"`
Value string `json:"value"`
}

const (
firing int = 0x01
pending int = 0x02
silenced int = 0x04
)

func stateToString(state int) string {
if state&firing > 0 {
return "firing"
}
if state&pending > 0 {
return "pending"
}
if state&silenced > 0 {
return "silenced"
}
return "inactive"
}

func randomState() int {
rndState := rand.IntN(10)
if rndState < 1 {
return silenced
} else if rndState < 3 {
return pending
}
return firing
}

func createAlert(probability float64, name, resourceName string, threshold, upperBound int, targetLabels, resourceNames []string, annotations, labels model.LabelSet) (*Alert, int) {
if rand.Float64() < probability {
alertLabels := labels.Clone()
alertState := randomState()
alertLabels["alertname"] = model.LabelValue(name)
for i, lbl := range targetLabels {
// First label will be "resourceName"; next are picked randomly
if i == 0 {
alertLabels[model.LabelName(lbl)] = model.LabelValue(resourceName)
} else {
idx := rand.IntN(len(resourceNames))
alertLabels[model.LabelName(lbl)] = model.LabelValue(resourceNames[idx])
}
}
val := float64(threshold) + rand.Float64()*float64(upperBound-threshold)
return &Alert{
Annotations: annotations,
Labels: alertLabels,
State: stateToString(alertState),
Value: fmt.Sprintf("%f", val),
}, alertState
}
return nil, 0
}

func createAlerts(probability float64, name string, threshold, upperBound int, targetLabels, resourceNames []string, annotations, labels model.LabelSet) ([]*Alert, int) {
alerts := []*Alert{}
var ruleState int
for _, resourceName := range resourceNames {
if alert, state := createAlert(probability, name, resourceName, threshold, upperBound, targetLabels, resourceNames, annotations, labels); alert != nil {
ruleState |= state
alerts = append(alerts, alert)
}
}
return alerts, ruleState
}

func createRule(probability float64, name, severity, extraFilter string, threshold, upperBound int, bynetobs bool, nsLbl, nodeLbl []string) AlertingRule {
labels := model.LabelSet{
"severity": model.LabelValue(severity),
}
annotations := model.LabelSet{
"description": model.LabelValue(name + " (a complete description...)"),
"summary": model.LabelValue(name + " (a summary...)"),
}
if bynetobs {
labels["app"] = "netobserv"
}
labels["netobserv"] = "true"
var jsonNsLbl, jsonNodeLbl string
if len(nsLbl) > 0 {
var quotedLbl []string
for _, lbl := range nsLbl {
quotedLbl = append(quotedLbl, `"`+lbl+`"`)
}
jsonNsLbl = fmt.Sprintf(`"namespaceLabels":[%s],`, strings.Join(quotedLbl, ","))
}
if len(nodeLbl) > 0 {
var quotedLbl []string
for _, lbl := range nodeLbl {
quotedLbl = append(quotedLbl, `"`+lbl+`"`)
}
jsonNodeLbl = fmt.Sprintf(`"nodeLabels":[%s],`, strings.Join(quotedLbl, ","))
}
searchURL := "https://duckduckgo.com/?q=" + url.PathEscape(name)
var extraFilterJSON string
if extraFilter != "" {
extraFilterJSON = fmt.Sprintf(`,"trafficLinkFilter":"%s"`, extraFilter)
}
annotations["netobserv_io_network_health"] = model.LabelValue(fmt.Sprintf(
`{%s%s"threshold":"%d","upperBound":"%d","unit":"%%","links":[{"name":"Search the web", "url": "%s"}]%s}`,
jsonNsLbl,
jsonNodeLbl,
threshold,
upperBound,
searchURL,
extraFilterJSON,
))
ruleLabels := labels.Clone()
ruleLabels["prometheus"] = "openshift-monitoring/k8s"
var alerts []*Alert
var ruleState int
if len(nsLbl) > 0 {
alerts, ruleState = createAlerts(probability, name, threshold, upperBound, nsLbl, namespaces, annotations, labels)
} else if len(nodeLbl) > 0 {
alerts, ruleState = createAlerts(probability, name, threshold, upperBound, nodeLbl, nodes, annotations, labels)
} else {
// global
alerts = []*Alert{}
if alert, state := createAlert(probability, name, "", threshold, upperBound, nil, nil, annotations, labels); alert != nil {
ruleState |= state
alerts = append(alerts, alert)
}
}
return AlertingRule{
Name: name,
Annotations: annotations,
Labels: labels,
State: stateToString(ruleState),
Alerts: alerts,
}
}

func GetRules() func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, _ *http.Request) {
alertingRules := []AlertingRule{
createRule(0.4, "Packet delivery failed", "info", "", 5, 100, true, []string{"SrcK8S_Namespace", "DstK8S_Namespace"}, []string{}),
createRule(0.3, "You have reached your hourly rate limit", "info", "", 5, 100, true, []string{"SrcK8S_Namespace", "DstK8S_Namespace"}, []string{}),
createRule(0.1, "It's always DNS", "warning", `dns_flag_response_code!=\"\"`, 15, 100, true, []string{"SrcK8S_Namespace", "DstK8S_Namespace"}, []string{}),
createRule(0.1, "We're under attack", "warning", "", 20, 100, true, []string{}, []string{}),
createRule(0.1, "Sh*t - Famous last words", "critical", "", 5, 100, true, []string{}, []string{"SrcK8S_Hostname", "DstK8S_Hostname"}),
createRule(0.3, "FromIngress", "info", "", 10, 100, false, []string{"exported_namespace"}, []string{}),
createRule(0.3, "Degraded latency", "info", "", 100, 1000, true, []string{"SrcK8S_Namespace", "DstK8S_Namespace"}, []string{}),
}
res := map[string]any{
"status": "success",
"data": map[string]any{
"groups": []map[string]any{
{
"name": "group-name",
"file": "file",
"interval": 30,
"rules": alertingRules,
},
},
},
}

response, err := json.Marshal(res)
if err != nil {
mlog.Errorf("Marshalling error while responding JSON: %v", err)
w.WriteHeader(http.StatusInternalServerError)
if _, err = w.Write([]byte(err.Error())); err != nil {
mlog.Errorf("Error while responding error: %v", err)
}
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err = w.Write(response); err != nil {
mlog.Errorf("Error while responding JSON: %v", err)
}
}
}
6 changes: 6 additions & 0 deletions pkg/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/netobserv/network-observability-console-plugin/pkg/config"
"github.com/netobserv/network-observability-console-plugin/pkg/handler"
"github.com/netobserv/network-observability-console-plugin/pkg/handler/alertingmock"
"github.com/netobserv/network-observability-console-plugin/pkg/kubernetes/auth"
"github.com/netobserv/network-observability-console-plugin/pkg/prometheus"
)
Expand Down Expand Up @@ -72,6 +73,11 @@ func setupRoutes(ctx context.Context, cfg *config.Config, authChecker auth.Check
r.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/dist/")))
}

if cfg.Loki.UseMocks {
// Add route for alerts (otherwise, the route is provided by the Console itself)
api.HandleFunc("/prometheus/api/v1/rules", alertingmock.GetRules())
}

return r
}

Expand Down
37 changes: 36 additions & 1 deletion web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,42 @@
"Previous tip": "Previous tip",
"Next tip": "Next tip",
"Close tips": "Close tips",
"Summary": "Summary",
"Navigate to alert details": "Navigate to alert details",
"State": "State",
"Severity": "Severity",
"Labels": "Labels",
"Description": "Description",
"Navigate to network traffic": "Navigate to network traffic",
"Global": "Global",
"critical issues": "critical issues",
"warnings": "warnings",
"minor issues": "minor issues",
"pending issues": "pending issues",
"silenced issues": "silenced issues",
"Score": "Score",
"Weight": "Weight",
"No violations found": "No violations found",
"Global rule violations": "Global rule violations",
"No rules found, health cannot be determined": "No rules found, health cannot be determined",
"Check alert definitions in FlowCollector \"spec.processor.metrics.alertGroups\" and \"spec.processor.metrics.disableAlerts\".": "Check alert definitions in FlowCollector \"spec.processor.metrics.alertGroups\" and \"spec.processor.metrics.disableAlerts\".",
"Make sure that Prometheus and AlertManager are running.": "Make sure that Prometheus and AlertManager are running.",
"The network looks healthy": "The network looks healthy",
"There are critical network issues": "There are critical network issues",
"There are network warnings": "There are network warnings",
"The network looks relatively healthy, with minor issues": "The network looks relatively healthy, with minor issues",
"{{firingAlerts}} critical issues, from {{firingRules}} distinct rules": "{{firingAlerts}} critical issues, from {{firingRules}} distinct rules",
"{{pendingAlerts}} pending critical issues, from {{pendingRules}} distinct rules": "{{pendingAlerts}} pending critical issues, from {{pendingRules}} distinct rules",
"No critical issues out of {{total}} rules": "No critical issues out of {{total}} rules",
"{{firingAlerts}} warnings, from {{firingRules}} distinct rules": "{{firingAlerts}} warnings, from {{firingRules}} distinct rules",
"{{pendingAlerts}} pending warnings, from {{pendingRules}} distinct rules": "{{pendingAlerts}} pending warnings, from {{pendingRules}} distinct rules",
"No warnings out of {{total}} rules": "No warnings out of {{total}} rules",
"{{firingAlerts}} minor issues, from {{firingRules}} distinct rules": "{{firingAlerts}} minor issues, from {{firingRules}} distinct rules",
"{{pendingAlerts}} pending minor issues, from {{pendingRules}} distinct rules": "{{pendingAlerts}} pending minor issues, from {{pendingRules}} distinct rules",
"No minor issues out of {{total}} rules": "No minor issues out of {{total}} rules",
"Network Health": "Network Health",
"Rule violations per node": "Rule violations per node",
"Rule violations per namespace": "Rule violations per namespace",
"No results found": "No results found",
"Clear or reset filters and try again.": "Clear or reset filters and try again.",
"Check for errors in health dashboard. Status endpoint is returning: {{statusError}}": "Check for errors in health dashboard. Status endpoint is returning: {{statusError}}",
Expand Down Expand Up @@ -335,7 +371,6 @@
"Filtered sum of packets": "Filtered sum of packets",
"Filtered average speed": "Filtered average speed",
"Bps": "Bps",
"Summary": "Summary",
"Query limit reached": "Query limit reached",
"Filtered flows count": "Filtered flows count",
"Filtered ended conversations count": "Filtered ended conversations count",
Expand Down
10 changes: 6 additions & 4 deletions web/src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,19 @@ export const getFlowRecords = (params: FlowQuery): Promise<RecordsResult> => {
});
};

export const getAlerts = (): Promise<AlertsResult> => {
return axios.get('/api/prometheus/api/v1/rules?type=alert').then(r => {
export const getAlerts = (match: string): Promise<AlertsResult> => {
const matchKeyEnc = encodeURIComponent('match[]');
const matchValEnc = encodeURIComponent('{' + match + '}');
return axios.get(`/api/prometheus/api/v1/rules?type=alert&${matchKeyEnc}=${matchValEnc}`).then(r => {
if (r.status >= 400) {
throw new Error(`${r.statusText} [code=${r.status}]`);
}
return r.data;
});
};

export const getSilencedAlerts = (): Promise<SilencedAlert[]> => {
return axios.get('/api/alertmanager/api/v2/silences').then(r => {
export const getSilencedAlerts = (match: string): Promise<SilencedAlert[]> => {
return axios.get(`/api/alertmanager/api/v2/silences?filter=${match}`).then(r => {
if (r.status >= 400) {
throw new Error(`${r.statusText} [code=${r.status}]`);
}
Expand Down
8 changes: 8 additions & 0 deletions web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import FlowCollectorStatus from './components/forms/flowCollector-status';
import FlowCollectorWizard from './components/forms/flowCollector-wizard';
import FlowMetricForm from './components/forms/flowMetric';
import FlowMetricWizard from './components/forms/flowMetric-wizard';
import NetworkHealth from './components/health/network-health';
import NetflowTrafficDevTab from './components/netflow-traffic-dev-tab';
import NetflowTrafficParent from './components/netflow-traffic-parent';
import NetflowTab from './components/netflow-traffic-tab';
Expand Down Expand Up @@ -61,6 +62,10 @@ export const pages = [
id: 'udn-tab',
name: 'UDN tab'
},
{
id: 'network-health',
name: 'Network Health'
},
{
id: 'flowCollector-wizard',
name: 'FlowCollector wizard'
Expand Down Expand Up @@ -199,6 +204,8 @@ export const App: React.FunctionComponent = () => {
return <FlowMetricWizard name="flowmetric-sample" />;
case 'flowMetric':
return <FlowMetricForm name="flowmetric-sample" />;
case 'network-health':
return <NetworkHealth />;
default:
return <NetflowTrafficParent />;
}
Expand All @@ -215,6 +222,7 @@ export const App: React.FunctionComponent = () => {
case 'flowCollector-status':
case 'flowMetric-wizard':
case 'flowMetric':
case 'network-health':
return <>{content}</>;
default:
return (
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/alerts/fetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ export const AlertFetcher: React.FC<AlertFetcherProps> = ({ children }) => {
const [alerts, setAlerts] = React.useState<Rule[]>([]);
const [silencedAlerts, setSilencedAlerts] = React.useState<string[] | null>(null);
React.useEffect(() => {
getAlerts()
getAlerts('app="netobserv"') // matching app="netobserv" catches all netobserv-owned alerts
.then(result => {
setAlerts(
result.data.groups.flatMap(group => {
return group.rules
.filter(rule => !!rule.labels.app && rule.labels.app == 'netobserv' && rule.state == 'firing')
.filter(rule => rule.state == 'firing' && !('netobserv_io_network_health' in rule.annotations))
.map(rule => {
const key = [
group.file,
Expand All @@ -41,7 +41,7 @@ export const AlertFetcher: React.FC<AlertFetcherProps> = ({ children }) => {
}, []);

React.useEffect(() => {
getSilencedAlerts()
getSilencedAlerts('app=netobserv')
.then(result => {
setSilencedAlerts(
result
Expand Down
Loading