Merge branch 'main' into ifrost/graphite-on-react-6
This commit is contained in:
@@ -91,7 +91,7 @@ shellcheck: $(SH_FILES) ## Run checks for shell scripts.
|
|||||||
build-docker-dev: ## Build Docker image for development (fast).
|
build-docker-dev: ## Build Docker image for development (fast).
|
||||||
@echo "build development container"
|
@echo "build development container"
|
||||||
@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
|
@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
|
||||||
$(GO) run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest
|
$(GO) run build.go -goos linux -pkg-arch amd64 ${OPT} build latest
|
||||||
cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||||
cd packaging/docker && docker build --tag grafana/grafana:dev .
|
cd packaging/docker && docker build --tag grafana/grafana:dev .
|
||||||
|
|
||||||
|
|||||||
@@ -715,6 +715,11 @@ global_session = -1
|
|||||||
# global limit of alerts
|
# global limit of alerts
|
||||||
global_alert_rule = -1
|
global_alert_rule = -1
|
||||||
|
|
||||||
|
#################################### Unified Alerting ####################
|
||||||
|
[unified_alerting]
|
||||||
|
# Specify the frequency of polling for admin config changes.
|
||||||
|
admin_config_poll_interval_seconds = 60
|
||||||
|
|
||||||
#################################### Alerting ############################
|
#################################### Alerting ############################
|
||||||
[alerting]
|
[alerting]
|
||||||
# Disable alerting engine & UI features
|
# Disable alerting engine & UI features
|
||||||
|
|||||||
@@ -698,6 +698,11 @@
|
|||||||
# global limit of alerts
|
# global limit of alerts
|
||||||
;global_alert_rule = -1
|
;global_alert_rule = -1
|
||||||
|
|
||||||
|
#################################### Unified Alerting ####################
|
||||||
|
[unified_alerting]
|
||||||
|
# Specify the frequency of polling for admin config changes.
|
||||||
|
;admin_config_poll_interval_seconds = 60
|
||||||
|
|
||||||
#################################### Alerting ############################
|
#################################### Alerting ############################
|
||||||
[alerting]
|
[alerting]
|
||||||
# Disable alerting engine & UI features
|
# Disable alerting engine & UI features
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ The following guidelines are specific to Grafana documentation. For the most par
|
|||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
||||||
|
Per the [Voice and tone](https://developers.google.com/style/tone) section of the Google developer documentation style guide:
|
||||||
|
|
||||||
|
> In your documents, aim for a voice and tone that's conversational, friendly, and respectful without being overly colloquial or frivolous; a voice that's casual and natural and approachable, not pedantic or pushy. Try to sound like a knowledgeable friend who understands what the developer wants to do.
|
||||||
|
|
||||||
- Use active voice:
|
- Use active voice:
|
||||||
- Active: Grafana displays the heatmap visualization.
|
- Active: Grafana displays the heatmap visualization.
|
||||||
- Passive: The heatmap visualization is displayed.
|
- Passive: The heatmap visualization is displayed.
|
||||||
|
|||||||
+104
-1
@@ -73,7 +73,7 @@ Family: scuemata.#Family & {
|
|||||||
schemaVersion: number | *30
|
schemaVersion: number | *30
|
||||||
// Version of the dashboard, incremented each time the dashboard is updated.
|
// Version of the dashboard, incremented each time the dashboard is updated.
|
||||||
version?: number
|
version?: number
|
||||||
panels?: [...#Panel]
|
panels?: [...(#Panel | #GraphPanel | #RowPanel)]
|
||||||
|
|
||||||
// TODO docs
|
// TODO docs
|
||||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(targetType="enum")
|
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(targetType="enum")
|
||||||
@@ -191,6 +191,12 @@ Family: scuemata.#Family & {
|
|||||||
// TODO docs
|
// TODO docs
|
||||||
maxDataPoints?: number
|
maxDataPoints?: number
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
thresholds?: [...]
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
timeRegions?: [...]
|
||||||
|
|
||||||
// TODO docs
|
// TODO docs
|
||||||
// TODO tighter constraint
|
// TODO tighter constraint
|
||||||
interval?: string
|
interval?: string
|
||||||
@@ -205,6 +211,7 @@ Family: scuemata.#Family & {
|
|||||||
|
|
||||||
// The values depend on panel type
|
// The values depend on panel type
|
||||||
options: {...}
|
options: {...}
|
||||||
|
|
||||||
fieldConfig: {
|
fieldConfig: {
|
||||||
defaults: {
|
defaults: {
|
||||||
// The display value for this field. This supports template variables blank is auto
|
// The display value for this field. This supports template variables blank is auto
|
||||||
@@ -289,6 +296,102 @@ Family: scuemata.#Family & {
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Row panel
|
||||||
|
#RowPanel: {
|
||||||
|
type: "row"
|
||||||
|
collapsed: bool | *false
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
// Name of default datasource.
|
||||||
|
datasource?: string
|
||||||
|
|
||||||
|
gridPos?: {
|
||||||
|
// Panel
|
||||||
|
h: number & >0 | *9
|
||||||
|
// Panel
|
||||||
|
w: number & >0 & <=24 | *12
|
||||||
|
// Panel x
|
||||||
|
x: number & >=0 & <24 | *0
|
||||||
|
// Panel y
|
||||||
|
y: number & >=0 | *0
|
||||||
|
// true if fixed
|
||||||
|
static?: bool
|
||||||
|
}
|
||||||
|
id: number
|
||||||
|
panels: [...#Panel | #GraphPanel]
|
||||||
|
}
|
||||||
|
// Support for legacy graph panels.
|
||||||
|
#GraphPanel: {
|
||||||
|
...
|
||||||
|
type: "graph"
|
||||||
|
thresholds: [...{...}]
|
||||||
|
timeRegions: [...{...}]
|
||||||
|
// FIXME this one is quite complicated, as it duplicates the #Panel object's own structure (...?)
|
||||||
|
seriesOverrides: [...{...}]
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
// TODO tighter constraint
|
||||||
|
aliasColors?: [string]: string
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
bars: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
dashes: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
dashLength: number | *10
|
||||||
|
// TODO docs
|
||||||
|
// TODO tighter constraint
|
||||||
|
fill?: number
|
||||||
|
// TODO docs
|
||||||
|
// TODO tighter constraint
|
||||||
|
fillGradient?: number
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
hiddenSeries: bool | *false
|
||||||
|
|
||||||
|
// FIXME idk where this comes from, leaving it very open and very wrong for now
|
||||||
|
legend: {...}
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
// TODO tighter constraint
|
||||||
|
lines: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
linewidth?: number
|
||||||
|
// TODO docs
|
||||||
|
nullPointMode: *"null" | "connected" | "null as zero"
|
||||||
|
// TODO docs
|
||||||
|
percentage: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
points: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
// FIXME this is the kind of case that makes
|
||||||
|
// optional/non-default tricky: it's optional because it
|
||||||
|
// only makes sense when points is true (right?), but if it
|
||||||
|
// is, then there actually is a default value. Easier way to
|
||||||
|
// represent this would be to wrap up this handling into a
|
||||||
|
// struct
|
||||||
|
pointradius?: number
|
||||||
|
// TODO docs
|
||||||
|
// TODO tighter constraint
|
||||||
|
renderer: string
|
||||||
|
// TODO docs
|
||||||
|
spaceLength: number | *10
|
||||||
|
// TODO docs
|
||||||
|
stack: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
steppedLine: bool | *false
|
||||||
|
// TODO docs
|
||||||
|
tooltip?: {
|
||||||
|
// TODO docs
|
||||||
|
shared?: bool
|
||||||
|
// TODO docs
|
||||||
|
sort: number | *0
|
||||||
|
// TODO docs
|
||||||
|
// FIXME literally no idea if these values are sane
|
||||||
|
value_type: *"individual" | "cumulative"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
+3
-1
@@ -40,7 +40,7 @@ LineConfig: {
|
|||||||
lineWidth?: number
|
lineWidth?: number
|
||||||
lineInterpolation?: LineInterpolation
|
lineInterpolation?: LineInterpolation
|
||||||
lineStyle?: LineStyle
|
lineStyle?: LineStyle
|
||||||
spanNulls?: bool
|
spanNulls?: bool | number
|
||||||
} @cuetsy(targetType="interface")
|
} @cuetsy(targetType="interface")
|
||||||
FillConfig: {
|
FillConfig: {
|
||||||
fillColor?: string
|
fillColor?: string
|
||||||
@@ -86,6 +86,8 @@ GraphFieldConfig: LineConfig & FillConfig & PointsConfig & AxisConfig & {
|
|||||||
VizLegendOptions: {
|
VizLegendOptions: {
|
||||||
displayMode: LegendDisplayMode
|
displayMode: LegendDisplayMode
|
||||||
placement: LegendPlacement
|
placement: LegendPlacement
|
||||||
|
asTable: bool | *false
|
||||||
|
isVisible: bool | *false
|
||||||
calcs: [...string]
|
calcs: [...string]
|
||||||
} @cuetsy(targetType="interface")
|
} @cuetsy(targetType="interface")
|
||||||
VizTooltipOptions: {
|
VizTooltipOptions: {
|
||||||
|
|||||||
@@ -10485,7 +10485,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -10613,7 +10613,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -259,10 +259,9 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false
|
||||||
"viz": false
|
|
||||||
},
|
},
|
||||||
"lineInterpolation": "linear",
|
"lineInterpolation": "linear",
|
||||||
"lineWidth": 1,
|
"lineWidth": 1,
|
||||||
@@ -549,7 +548,7 @@
|
|||||||
"fillOpacity": 74,
|
"fillOpacity": 74,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -665,7 +664,7 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -806,7 +805,7 @@
|
|||||||
"fillOpacity": 50,
|
"fillOpacity": 50,
|
||||||
"gradientMode": "hue",
|
"gradientMode": "hue",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -924,7 +923,7 @@
|
|||||||
"fillOpacity": 40,
|
"fillOpacity": 40,
|
||||||
"gradientMode": "hue",
|
"gradientMode": "hue",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -1042,7 +1041,7 @@
|
|||||||
"fillOpacity": 40,
|
"fillOpacity": 40,
|
||||||
"gradientMode": "hue",
|
"gradientMode": "hue",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -1183,12 +1182,9 @@
|
|||||||
"barAlignment": 0,
|
"barAlignment": 0,
|
||||||
"drawStyle": "line",
|
"drawStyle": "line",
|
||||||
"fillOpacity": 10,
|
"fillOpacity": 10,
|
||||||
"gradientMode": {
|
"gradientMode": "none",
|
||||||
"label": "None",
|
|
||||||
"value": "none"
|
|
||||||
},
|
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -1289,12 +1285,9 @@
|
|||||||
"barAlignment": 0,
|
"barAlignment": 0,
|
||||||
"drawStyle": "line",
|
"drawStyle": "line",
|
||||||
"fillOpacity": 10,
|
"fillOpacity": 10,
|
||||||
"gradientMode": {
|
"gradientMode": "none",
|
||||||
"label": "None",
|
|
||||||
"value": "none"
|
|
||||||
},
|
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -1399,12 +1392,9 @@
|
|||||||
"barAlignment": 0,
|
"barAlignment": 0,
|
||||||
"drawStyle": "line",
|
"drawStyle": "line",
|
||||||
"fillOpacity": 10,
|
"fillOpacity": 10,
|
||||||
"gradientMode": {
|
"gradientMode": "none",
|
||||||
"label": "None",
|
|
||||||
"value": "none"
|
|
||||||
},
|
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -1533,7 +1523,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "opacity",
|
"gradientMode": "opacity",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -1768,12 +1758,9 @@
|
|||||||
"barAlignment": 0,
|
"barAlignment": 0,
|
||||||
"drawStyle": "line",
|
"drawStyle": "line",
|
||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": {
|
"gradientMode": "none",
|
||||||
"label": "None",
|
|
||||||
"value": "none"
|
|
||||||
},
|
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -2093,12 +2080,9 @@
|
|||||||
"barAlignment": 0,
|
"barAlignment": 0,
|
||||||
"drawStyle": "line",
|
"drawStyle": "line",
|
||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": {
|
"gradientMode": "none",
|
||||||
"label": "None",
|
|
||||||
"value": "none"
|
|
||||||
},
|
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -2420,7 +2404,7 @@
|
|||||||
"fillOpacity": 82,
|
"fillOpacity": 82,
|
||||||
"gradientMode": "hue",
|
"gradientMode": "hue",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
@@ -2566,7 +2550,7 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "hue",
|
"gradientMode": "hue",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false
|
"tooltip": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -106,7 +105,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -177,7 +175,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -247,7 +244,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -318,7 +314,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -388,7 +383,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -460,7 +454,6 @@
|
|||||||
"fillOpacity": 80,
|
"fillOpacity": 80,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -507,7 +507,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -623,7 +623,7 @@
|
|||||||
"fillOpacity": 0,
|
"fillOpacity": 0,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -758,7 +758,7 @@
|
|||||||
"fillOpacity": 10,
|
"fillOpacity": 10,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -902,7 +902,7 @@
|
|||||||
"fillOpacity": 10,
|
"fillOpacity": 10,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
@@ -1045,7 +1045,7 @@
|
|||||||
"fillOpacity": 10,
|
"fillOpacity": 10,
|
||||||
"gradientMode": "none",
|
"gradientMode": "none",
|
||||||
"hideFrom": {
|
"hideFrom": {
|
||||||
"graph": false,
|
"viz": false,
|
||||||
"legend": false,
|
"legend": false,
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
"viz": false
|
"viz": false
|
||||||
|
|||||||
@@ -49,10 +49,6 @@
|
|||||||
},
|
},
|
||||||
"showPoints": "auto",
|
"showPoints": "auto",
|
||||||
"spanNulls": false,
|
"spanNulls": false,
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
"thresholdsStyle": {
|
||||||
"mode": "line"
|
"mode": "line"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
influxdb1:
|
influxdb1:
|
||||||
image: influxdb:1.8.6
|
image: influxdb:1.8
|
||||||
container_name: influxdb1
|
container_name: influxdb1
|
||||||
ports:
|
ports:
|
||||||
- '2004:2004'
|
- '2004:2004'
|
||||||
|
|||||||
@@ -1099,6 +1099,16 @@ Sets a global limit on number of alert rules that can be created. Default is -1
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
## [unified_alerting]
|
||||||
|
|
||||||
|
For more information about the Unified Alerting feature in Grafana, refer to [Unified Alerting]({{< relref "../unified-alerting/_index.md" >}}}).
|
||||||
|
|
||||||
|
### admin_config_poll_interval_seconds
|
||||||
|
|
||||||
|
Specify the frequency of polling for admin config changes. The default value is `60`.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
## [alerting]
|
## [alerting]
|
||||||
|
|
||||||
For more information about the Alerting feature in Grafana, refer to [Alerts overview]({{< relref "../alerting/_index.md" >}}).
|
For more information about the Alerting feature in Grafana, refer to [Alerts overview]({{< relref "../alerting/_index.md" >}}).
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ To set up PagerDuty, all you have to do is to provide an integration key.
|
|||||||
|
|
||||||
> **Note:** The `state` tag overrides the current alert state inside the `custom_details` payload.
|
> **Note:** The `state` tag overrides the current alert state inside the `custom_details` payload.
|
||||||
|
|
||||||
|
> **Note:** Grafana uses the `Events API V2` integration. This can be configured for each service.
|
||||||
|
|
||||||
### VictorOps
|
### VictorOps
|
||||||
|
|
||||||
To configure VictorOps, provide the URL from the Grafana Integration and substitute `$routing_key` with a valid key.
|
To configure VictorOps, provide the URL from the Grafana Integration and substitute `$routing_key` with a valid key.
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ query(options: DataQueryRequest<MyQuery>): Observable<DataQueryResponse> {
|
|||||||
subscriber.next({
|
subscriber.next({
|
||||||
data: [frame],
|
data: [frame],
|
||||||
key: query.refId,
|
key: query.refId,
|
||||||
|
state: LoadingState.Streaming,
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ To verify ownership of your plugin, you need to generate an API key that you'll
|
|||||||
|
|
||||||
You can find the plugin ID in the `plugin.json` file inside your plugin directory. For example, if your account slug is `acmecorp`, you need to prefix the plugin ID with `acmecorp-`.
|
You can find the plugin ID in the `plugin.json` file inside your plugin directory. For example, if your account slug is `acmecorp`, you need to prefix the plugin ID with `acmecorp-`.
|
||||||
|
|
||||||
1. [Create a Grafana Cloud API key](https://grafana.com/docs/grafana-cloud/cloud-portal/create-api-key/) with the **PluginPublisher** role.
|
1. [Create a Grafana Cloud API key](https://grafana.com/docs/grafana-cloud/reference/create-api-key/) with the **PluginPublisher** role.
|
||||||
|
|
||||||
## Sign a public plugin
|
## Sign a public plugin
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ weight = 200
|
|||||||
|
|
||||||
# Add a custom variable
|
# Add a custom variable
|
||||||
|
|
||||||
Use a _custom_ variable for values that do not change. This might be numbers, strings, or even other variables.
|
Use a _custom_ variable for a value that does not change, such as a number or a string.
|
||||||
|
|
||||||
For example, if you have server names or region names that never change, then you might want to create them as custom variables rather than query variables. Because they do not change, you might use them in [chained variables]({{< relref "chained-variables.md" >}}) rather than other query variables. That would reduce the number of queries Grafana must send when chained variables are updated.
|
For example, if you have server names or region names that never change, then you might want to create them as custom variables rather than query variables. Because they do not change, you might use them in [chained variables]({{< relref "chained-variables.md" >}}) rather than other query variables. That would reduce the number of queries Grafana must send when chained variables are updated.
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ For example, if you have server names or region names that never change, then yo
|
|||||||
|
|
||||||
## Enter Custom Options
|
## Enter Custom Options
|
||||||
|
|
||||||
1. In the **Values separated by comma** list, enter the values for this variable in a comma-separated list. You can include numbers, strings, other variables or key/value pairs separated by a space and a colon, i.e. `key1 : value1,key2 : value2`.
|
1. In the **Values separated by comma** list, enter the values for this variable in a comma-separated list. You can include numbers, strings, or key/value pairs separated by a space and a colon. For example, `key1 : value1,key2 : value2`.
|
||||||
1. (optional) Enter [Selection Options]({{< relref "../variable-selection-options.md" >}}).
|
1. (optional) Enter [Selection Options]({{< relref "../variable-selection-options.md" >}}).
|
||||||
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
|
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
|
||||||
1. Click **Add** to add the variable to the dashboard.
|
1. Click **Add** to add the variable to the dashboard.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/gosimple/slug v1.9.0
|
github.com/gosimple/slug v1.9.0
|
||||||
github.com/grafana/grafana-aws-sdk v0.7.0
|
github.com/grafana/grafana-aws-sdk v0.7.0
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.111.0
|
github.com/grafana/grafana-plugin-sdk-go v0.112.0
|
||||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103
|
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
github.com/hashicorp/go-hclog v0.16.1
|
github.com/hashicorp/go-hclog v0.16.1
|
||||||
@@ -67,6 +67,7 @@ require (
|
|||||||
github.com/laher/mergefs v0.1.1
|
github.com/laher/mergefs v0.1.1
|
||||||
github.com/lib/pq v1.10.0
|
github.com/lib/pq v1.10.0
|
||||||
github.com/linkedin/goavro/v2 v2.10.0
|
github.com/linkedin/goavro/v2 v2.10.0
|
||||||
|
github.com/m3db/prometheus_remote_client_golang v0.4.4 // indirect
|
||||||
github.com/magefile/mage v1.11.0
|
github.com/magefile/mage v1.11.0
|
||||||
github.com/mattn/go-isatty v0.0.12
|
github.com/mattn/go-isatty v0.0.12
|
||||||
github.com/mattn/go-sqlite3 v1.14.7
|
github.com/mattn/go-sqlite3 v1.14.7
|
||||||
|
|||||||
@@ -995,6 +995,8 @@ github.com/grafana/grafana-aws-sdk v0.7.0/go.mod h1:+pPo5U+pX0zWimR7YBc7ASeSQfbR
|
|||||||
github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60=
|
github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.111.0 h1:7eqLgAdDYeELVokRCeDEobYQJy5/E5C43tZkb/xH66Q=
|
github.com/grafana/grafana-plugin-sdk-go v0.111.0 h1:7eqLgAdDYeELVokRCeDEobYQJy5/E5C43tZkb/xH66Q=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.111.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
github.com/grafana/grafana-plugin-sdk-go v0.111.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||||
|
github.com/grafana/grafana-plugin-sdk-go v0.112.0 h1:c+3kKJQ1wVZ5pMSF40eUizB5YzGPAbO+2QG0CJ3d4bI=
|
||||||
|
github.com/grafana/grafana-plugin-sdk-go v0.112.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E=
|
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E=
|
||||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103/go.mod h1:GHIsn+EohCChsdu5YouNZewqLeV9L2FNw4DEJU3P9qE=
|
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103/go.mod h1:GHIsn+EohCChsdu5YouNZewqLeV9L2FNw4DEJU3P9qE=
|
||||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
@@ -1327,6 +1329,8 @@ github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1
|
|||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/lufia/iostat v1.1.0/go.mod h1:rEPNA0xXgjHQjuI5Cy05sLlS2oRcSlWHRLrvh/AQ+Pg=
|
github.com/lufia/iostat v1.1.0/go.mod h1:rEPNA0xXgjHQjuI5Cy05sLlS2oRcSlWHRLrvh/AQ+Pg=
|
||||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||||
|
github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1OuMLIdIikAEHeNPHY+yyM8=
|
||||||
|
github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc=
|
||||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
|
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
|
||||||
|
|||||||
+5
-5
@@ -183,7 +183,7 @@
|
|||||||
"mutationobserver-shim": "0.3.3",
|
"mutationobserver-shim": "0.3.3",
|
||||||
"ngtemplate-loader": "2.0.1",
|
"ngtemplate-loader": "2.0.1",
|
||||||
"nodemon": "2.0.2",
|
"nodemon": "2.0.2",
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.4",
|
"optimize-css-assets-webpack-plugin": "5.0.5",
|
||||||
"postcss-browser-reporter": "0.6.0",
|
"postcss-browser-reporter": "0.6.0",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "3.0.0",
|
||||||
"postcss-reporter": "6.0.1",
|
"postcss-reporter": "6.0.1",
|
||||||
@@ -221,6 +221,9 @@
|
|||||||
"@emotion/react": "11.1.5",
|
"@emotion/react": "11.1.5",
|
||||||
"@grafana/aws-sdk": "0.0.3",
|
"@grafana/aws-sdk": "0.0.3",
|
||||||
"@grafana/slate-react": "0.22.10-grafana",
|
"@grafana/slate-react": "0.22.10-grafana",
|
||||||
|
"@opentelemetry/api": "1.0.2",
|
||||||
|
"@opentelemetry/exporter-collector": "0.23.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "0.23.0",
|
||||||
"@popperjs/core": "2.5.4",
|
"@popperjs/core": "2.5.4",
|
||||||
"@reduxjs/toolkit": "1.5.0",
|
"@reduxjs/toolkit": "1.5.0",
|
||||||
"@sentry/browser": "5.25.0",
|
"@sentry/browser": "5.25.0",
|
||||||
@@ -265,12 +268,9 @@
|
|||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"mousetrap-global-bind": "1.1.0",
|
"mousetrap-global-bind": "1.1.0",
|
||||||
"ol": "^6.5.0",
|
"ol": "^6.5.0",
|
||||||
"@opentelemetry/api": "1.0.2",
|
|
||||||
"@opentelemetry/exporter-collector": "0.23.0",
|
|
||||||
"@opentelemetry/semantic-conventions": "0.23.0",
|
|
||||||
"papaparse": "5.3.0",
|
"papaparse": "5.3.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"prismjs": "1.23.0",
|
"prismjs": "1.24.0",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"rc-cascader": "1.0.1",
|
"rc-cascader": "1.0.1",
|
||||||
"re-resizable": "^6.2.0",
|
"re-resizable": "^6.2.0",
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ export function emphasize(color: string, coefficient = 0.15) {
|
|||||||
* @beta
|
* @beta
|
||||||
*/
|
*/
|
||||||
export function alpha(color: string, value: number) {
|
export function alpha(color: string, value: number) {
|
||||||
|
if (color === '') {
|
||||||
|
return '#000000';
|
||||||
|
}
|
||||||
|
|
||||||
value = clamp(value);
|
value = clamp(value);
|
||||||
|
|
||||||
// hex 6, hex 8 (w/alpha)
|
// hex 6, hex 8 (w/alpha)
|
||||||
|
|||||||
@@ -1,33 +1,28 @@
|
|||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
import { seriesToColumnsTransformer } from './seriesToColumns';
|
import { seriesToColumnsTransformer } from './seriesToColumns';
|
||||||
import { DataFrame } from '../../types/dataFrame';
|
import { DataFrame } from '../../types/dataFrame';
|
||||||
import { getTimeField } from '../../dataframe/processDataFrame';
|
import { getTimeField } from '../../dataframe/processDataFrame';
|
||||||
import { DataTransformerInfo } from '../../types/transformations';
|
import { SynchronousDataTransformerInfo } from '../../types/transformations';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { mergeMap } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
export const ensureColumnsTransformer: DataTransformerInfo = {
|
export const ensureColumnsTransformer: SynchronousDataTransformerInfo = {
|
||||||
id: DataTransformerID.ensureColumns,
|
id: DataTransformerID.ensureColumns,
|
||||||
name: 'Ensure Columns Transformer',
|
name: 'Ensure Columns Transformer',
|
||||||
description: 'Will check if current data frames is series or columns. If in series it will convert to columns.',
|
description: 'Will check if current data frames is series or columns. If in series it will convert to columns.',
|
||||||
operator: (options = {}) => (source) =>
|
|
||||||
source.pipe(
|
|
||||||
mergeMap((data) => {
|
|
||||||
// Assume timeseries should first be joined by time
|
|
||||||
const timeFieldName = findConsistentTimeFieldName(data);
|
|
||||||
|
|
||||||
if (data.length > 1 && timeFieldName) {
|
operator: (options) => (source) => source.pipe(map((data) => ensureColumnsTransformer.transformer(options)(data))),
|
||||||
return of(data).pipe(
|
|
||||||
seriesToColumnsTransformer.operator({
|
|
||||||
byField: timeFieldName,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return of(data);
|
transformer: (options: any) => (frames: DataFrame[]) => {
|
||||||
})
|
// Assume timeseries should first be joined by time
|
||||||
),
|
const timeFieldName = findConsistentTimeFieldName(frames);
|
||||||
|
|
||||||
|
if (frames.length > 1 && timeFieldName) {
|
||||||
|
return seriesToColumnsTransformer.transformer({
|
||||||
|
byField: timeFieldName,
|
||||||
|
})(frames);
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DataTransformerInfo } from '../../types';
|
import { SynchronousDataTransformerInfo } from '../../types';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
@@ -73,7 +73,7 @@ export const histogramFieldInfo = {
|
|||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export const histogramTransformer: DataTransformerInfo<HistogramTransformerOptions> = {
|
export const histogramTransformer: SynchronousDataTransformerInfo<HistogramTransformerOptions> = {
|
||||||
id: DataTransformerID.histogram,
|
id: DataTransformerID.histogram,
|
||||||
name: 'Histogram',
|
name: 'Histogram',
|
||||||
description: 'Calculate a histogram from input data',
|
description: 'Calculate a histogram from input data',
|
||||||
@@ -81,23 +81,18 @@ export const histogramTransformer: DataTransformerInfo<HistogramTransformerOptio
|
|||||||
fields: {},
|
fields: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
operator: (options) => (source) => source.pipe(map((data) => histogramTransformer.transformer(options)(data))),
|
||||||
* Return a modified copy of the series. If the transform is not or should not
|
|
||||||
* be applied, just return the input series
|
transformer: (options: HistogramTransformerOptions) => (data: DataFrame[]) => {
|
||||||
*/
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
operator: (options) => (source) =>
|
return data;
|
||||||
source.pipe(
|
}
|
||||||
map((data) => {
|
const hist = buildHistogram(data, options);
|
||||||
if (!Array.isArray(data) || data.length === 0) {
|
if (hist == null) {
|
||||||
return data;
|
return [];
|
||||||
}
|
}
|
||||||
const hist = buildHistogram(data, options);
|
return [histogramFieldsToFrame(hist)];
|
||||||
if (hist == null) {
|
},
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [histogramFieldsToFrame(hist)];
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataTransformerInfo, FieldMatcher } from '../../types';
|
import { DataFrame, SynchronousDataTransformerInfo, FieldMatcher } from '../../types';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { outerJoinDataFrames } from './joinDataFrames';
|
import { outerJoinDataFrames } from './joinDataFrames';
|
||||||
import { fieldMatchers } from '../matchers';
|
import { fieldMatchers } from '../matchers';
|
||||||
@@ -10,27 +10,29 @@ export interface SeriesToColumnsOptions {
|
|||||||
byField?: string; // empty will pick the field automatically
|
byField?: string; // empty will pick the field automatically
|
||||||
}
|
}
|
||||||
|
|
||||||
export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = {
|
export const seriesToColumnsTransformer: SynchronousDataTransformerInfo<SeriesToColumnsOptions> = {
|
||||||
id: DataTransformerID.seriesToColumns,
|
id: DataTransformerID.seriesToColumns,
|
||||||
name: 'Series as columns', // Called 'Outer join' in the UI!
|
name: 'Series as columns', // Called 'Outer join' in the UI!
|
||||||
description: 'Groups series by field and returns values as columns',
|
description: 'Groups series by field and returns values as columns',
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
byField: undefined, // DEFAULT_KEY_FIELD,
|
byField: undefined, // DEFAULT_KEY_FIELD,
|
||||||
},
|
},
|
||||||
operator: (options) => (source) =>
|
|
||||||
source.pipe(
|
operator: (options) => (source) => source.pipe(map((data) => seriesToColumnsTransformer.transformer(options)(data))),
|
||||||
map((data) => {
|
|
||||||
if (data.length > 1) {
|
transformer: (options: SeriesToColumnsOptions) => {
|
||||||
let joinBy: FieldMatcher | undefined = undefined;
|
let joinBy: FieldMatcher | undefined = undefined;
|
||||||
if (options.byField) {
|
return (data: DataFrame[]) => {
|
||||||
joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField);
|
if (data.length > 1) {
|
||||||
}
|
if (options.byField && !joinBy) {
|
||||||
const joined = outerJoinDataFrames({ frames: data, joinBy });
|
joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField);
|
||||||
if (joined) {
|
|
||||||
return [joined];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return data;
|
const joined = outerJoinDataFrames({ frames: data, joinBy });
|
||||||
})
|
if (joined) {
|
||||||
),
|
return [joined];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { RegistryItemWithOptions } from '../utils/Registry';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that transform data frames (AKA transformer)
|
* Function that transform data frames (AKA transformer)
|
||||||
|
*
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface DataTransformerInfo<TOptions = any> extends RegistryItemWithOptions {
|
export interface DataTransformerInfo<TOptions = any> extends RegistryItemWithOptions {
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +16,19 @@ export interface DataTransformerInfo<TOptions = any> extends RegistryItemWithOpt
|
|||||||
operator: (options: TOptions) => MonoTypeOperatorFunction<DataFrame[]>;
|
operator: (options: TOptions) => MonoTypeOperatorFunction<DataFrame[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Many transformations can be called with a simple synchronous function.
|
||||||
|
* When a transformer is defined, it should have identical behavior to using the operator
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface SynchronousDataTransformerInfo<TOptions = any> extends DataTransformerInfo<TOptions> {
|
||||||
|
transformer: (options: TOptions) => (frames: DataFrame[]) => DataFrame[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface DataTransformerConfig<TOptions = any> {
|
export interface DataTransformerConfig<TOptions = any> {
|
||||||
/**
|
/**
|
||||||
* Unique identifier of transformer
|
* Unique identifier of transformer
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const Components = {
|
|||||||
},
|
},
|
||||||
Panels: {
|
Panels: {
|
||||||
Panel: {
|
Panel: {
|
||||||
title: (title: string) => `Panel header ${title}`,
|
title: (title: string) => `data-testid Panel header ${title}`,
|
||||||
headerItems: (item: string) => `Panel header item ${item}`,
|
headerItems: (item: string) => `Panel header item ${item}`,
|
||||||
containerByTitle: (title: string) => `${title} panel`,
|
containerByTitle: (title: string) => `${title} panel`,
|
||||||
headerCornerInfo: (mode: string) => `Panel header ${mode}`,
|
headerCornerInfo: (mode: string) => `Panel header ${mode}`,
|
||||||
|
|||||||
@@ -57,17 +57,19 @@ export const bundlePlugin = async ({ watch, production, preserveConsole }: Plugi
|
|||||||
} else {
|
} else {
|
||||||
compiler.run((err: Error, stats: webpack.Stats) => {
|
compiler.run((err: Error, stats: webpack.Stats) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err.message);
|
reject(err);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
if (stats.hasErrors()) {
|
||||||
stats.compilation.errors.forEach((e) => {
|
stats.compilation.errors.forEach((e) => {
|
||||||
console.log(e.message);
|
console.log(e.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
reject('Build failed');
|
reject('Build failed');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n', stats.toString({ colors: true }), '\n');
|
console.log('\n', stats.toString({ colors: true }), '\n');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export const useSpinner = async (label: string, fn: () => Promise<any>, killProc
|
|||||||
|
|
||||||
if (err.stdout) {
|
if (err.stdout) {
|
||||||
console.error(err.stdout);
|
console.error(err.stdout);
|
||||||
} else {
|
} else if (err.message) {
|
||||||
|
// Return stack trace if error object
|
||||||
console.trace(err); // eslint-disable-line no-console
|
console.trace(err); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
"react-transition-group": "4.4.1",
|
"react-transition-group": "4.4.1",
|
||||||
"slate": "0.47.8",
|
"slate": "0.47.8",
|
||||||
"tinycolor2": "1.4.1",
|
"tinycolor2": "1.4.1",
|
||||||
"uplot": "1.6.14"
|
"uplot": "1.6.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "16.0.0",
|
"@rollup/plugin-commonjs": "16.0.0",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export class MultiSelectValueEditor<T> extends React.PureComponent<Props<T>, Sta
|
|||||||
const { settings } = item;
|
const { settings } = item;
|
||||||
return (
|
return (
|
||||||
<MultiSelect<T>
|
<MultiSelect<T>
|
||||||
|
menuShouldPortal
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
value={value}
|
value={value}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export class SelectValueEditor<T> extends React.PureComponent<Props<T>, State<T>
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Select<T>
|
<Select<T>
|
||||||
|
menuShouldPortal
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
value={current}
|
value={current}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const PageToolbar: FC<Props> = React.memo(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<nav className={styles.navElement}>
|
<nav aria-label="Search links" className={styles.navElement}>
|
||||||
{parent && parentHref && (
|
{parent && parentHref && (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ LegendDisplayMode: "list" | "table" | "hidden" @cuetsy(targetType="enum")
|
|||||||
VizLegendOptions: {
|
VizLegendOptions: {
|
||||||
displayMode: LegendDisplayMode
|
displayMode: LegendDisplayMode
|
||||||
placement: LegendPlacement
|
placement: LegendPlacement
|
||||||
|
asTable: bool | *false
|
||||||
|
isVisible: bool | *false
|
||||||
calcs: [...string]
|
calcs: [...string]
|
||||||
|
|
||||||
} @cuetsy(targetType="interface")
|
} @cuetsy(targetType="interface")
|
||||||
|
|
||||||
// TODO this excludes all the types that include function definitions
|
// TODO this excludes all the types that include function definitions
|
||||||
@@ -436,7 +436,7 @@ describe('UPlotConfigBuilder', () => {
|
|||||||
theme: darkTheme,
|
theme: darkTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(builder.getConfig().series[1].fill).toBe('rgba(255, 170, 187, 0.5)');
|
expect(builder.getConfig().series[1].fill).toBe('#FFAABB80');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when fillColor is set ignore fillOpacity', () => {
|
it('when fillColor is set ignore fillOpacity', () => {
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ const cursorDefaults: Cursor = {
|
|||||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2,
|
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2,
|
||||||
/*@ts-ignore*/
|
/*@ts-ignore*/
|
||||||
width: (u, seriesIdx, size) => size / 4,
|
width: (u, seriesIdx, size) => size / 4,
|
||||||
/*@ts-ignore*/
|
|
||||||
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '80',
|
|
||||||
/*@ts-ignore*/
|
|
||||||
fill: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx),
|
|
||||||
},
|
},
|
||||||
focus: {
|
focus: {
|
||||||
prox: 30,
|
prox: 30,
|
||||||
@@ -50,6 +46,7 @@ export class UPlotConfigBuilder {
|
|||||||
private hooks: Hooks.Arrays = {};
|
private hooks: Hooks.Arrays = {};
|
||||||
private tz: string | undefined = undefined;
|
private tz: string | undefined = undefined;
|
||||||
private sync = false;
|
private sync = false;
|
||||||
|
private frame: DataFrame | undefined = undefined;
|
||||||
// to prevent more than one threshold per scale
|
// to prevent more than one threshold per scale
|
||||||
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
||||||
/**
|
/**
|
||||||
@@ -158,7 +155,10 @@ export class UPlotConfigBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPrepData(prepData: PrepData) {
|
setPrepData(prepData: PrepData) {
|
||||||
this.prepData = prepData;
|
this.prepData = (frame) => {
|
||||||
|
this.frame = frame;
|
||||||
|
return prepData(frame);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setSync() {
|
setSync() {
|
||||||
@@ -187,7 +187,25 @@ export class UPlotConfigBuilder {
|
|||||||
|
|
||||||
config.select = this.select;
|
config.select = this.select;
|
||||||
|
|
||||||
config.cursor = merge({}, cursorDefaults, this.cursor);
|
const pointColorFn = (alphaHex = '') => (u: uPlot, seriesIdx: number) => {
|
||||||
|
/*@ts-ignore*/
|
||||||
|
let s = u.series[seriesIdx].points._stroke;
|
||||||
|
|
||||||
|
// interpolate for gradients/thresholds
|
||||||
|
if (typeof s !== 'string') {
|
||||||
|
let field = this.frame!.fields[seriesIdx];
|
||||||
|
s = field.display!(field.values.get(u.cursor.idxs![seriesIdx]!)).color!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s + alphaHex;
|
||||||
|
};
|
||||||
|
|
||||||
|
config.cursor = merge({}, cursorDefaults, this.cursor, {
|
||||||
|
points: {
|
||||||
|
stroke: pointColorFn('80'),
|
||||||
|
fill: pointColorFn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
config.tzDate = this.tzDate;
|
config.tzDate = this.tzDate;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { DataFrameFieldIndex, FALLBACK_COLOR, FieldColorMode, GrafanaTheme2, ThresholdsConfig } from '@grafana/data';
|
import {
|
||||||
import tinycolor from 'tinycolor2';
|
colorManipulator,
|
||||||
|
DataFrameFieldIndex,
|
||||||
|
FALLBACK_COLOR,
|
||||||
|
FieldColorMode,
|
||||||
|
GrafanaTheme2,
|
||||||
|
ThresholdsConfig,
|
||||||
|
} from '@grafana/data';
|
||||||
import uPlot, { Series } from 'uplot';
|
import uPlot, { Series } from 'uplot';
|
||||||
import {
|
import {
|
||||||
BarAlignment,
|
BarAlignment,
|
||||||
@@ -55,16 +61,18 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let lineConfig: Partial<Series> = {};
|
let lineConfig: Partial<Series> = {};
|
||||||
const lineColor = this.getLineColor();
|
|
||||||
|
let lineColor = this.getLineColor();
|
||||||
|
|
||||||
|
// DrawStyle.Points mode also needs this for fill/stroke sharing & re-use in series.points. see getColor() below.
|
||||||
|
lineConfig.stroke = lineColor;
|
||||||
|
|
||||||
if (pathBuilder != null) {
|
if (pathBuilder != null) {
|
||||||
lineConfig.paths = pathBuilder;
|
lineConfig.paths = pathBuilder;
|
||||||
lineConfig.stroke = lineColor;
|
|
||||||
lineConfig.width = lineWidth;
|
lineConfig.width = lineWidth;
|
||||||
} else if (drawStyle === DrawStyle.Points) {
|
} else if (drawStyle === DrawStyle.Points) {
|
||||||
lineConfig.paths = () => null;
|
lineConfig.paths = () => null;
|
||||||
} else if (drawStyle != null) {
|
} else if (drawStyle != null) {
|
||||||
lineConfig.stroke = lineColor;
|
|
||||||
lineConfig.width = lineWidth;
|
lineConfig.width = lineWidth;
|
||||||
if (lineStyle && lineStyle.fill !== 'solid') {
|
if (lineStyle && lineStyle.fill !== 'solid') {
|
||||||
if (lineStyle.fill === 'dot') {
|
if (lineStyle.fill === 'dot') {
|
||||||
@@ -84,10 +92,14 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useColor: uPlot.Series.Stroke =
|
||||||
|
// @ts-ignore
|
||||||
|
typeof lineColor === 'string' ? lineColor : (u, seriesIdx) => u.series[seriesIdx]._stroke;
|
||||||
|
|
||||||
const pointsConfig: Partial<Series> = {
|
const pointsConfig: Partial<Series> = {
|
||||||
points: {
|
points: {
|
||||||
stroke: lineColor,
|
stroke: useColor,
|
||||||
fill: lineColor,
|
fill: useColor,
|
||||||
size: pointSize,
|
size: pointSize,
|
||||||
filter: pointsFilter,
|
filter: pointsFilter,
|
||||||
},
|
},
|
||||||
@@ -153,7 +165,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
|||||||
return getScaleGradientFn(opacityPercent, theme, colorMode, thresholds);
|
return getScaleGradientFn(opacityPercent, theme, colorMode, thresholds);
|
||||||
default:
|
default:
|
||||||
if (opacityPercent > 0) {
|
if (opacityPercent > 0) {
|
||||||
return tinycolor(lineColor).setAlpha(opacityPercent).toString();
|
return colorManipulator.alpha(lineColor ?? '', opacityPercent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getColorForTheme, GrafanaTheme2, ThresholdsConfig } from '@grafana/data';
|
import { GrafanaTheme2, ThresholdsConfig, ThresholdsMode } from '@grafana/data';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { GraphThresholdsStyleConfig, GraphTresholdsStyleMode } from '../config';
|
import { GraphThresholdsStyleConfig, GraphTresholdsStyleMode } from '../config';
|
||||||
|
import { getDataRange, GradientDirection, scaleGradient } from './gradientFills';
|
||||||
|
|
||||||
export interface UPlotThresholdOptions {
|
export interface UPlotThresholdOptions {
|
||||||
scaleKey: string;
|
scaleKey: string;
|
||||||
@@ -13,7 +14,6 @@ export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
|||||||
return (u: uPlot) => {
|
return (u: uPlot) => {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const { scaleKey, thresholds, theme, config } = options;
|
const { scaleKey, thresholds, theme, config } = options;
|
||||||
const { steps } = thresholds;
|
|
||||||
const { min: xMin, max: xMax } = u.scales.x;
|
const { min: xMin, max: xMax } = u.scales.x;
|
||||||
const { min: yMin, max: yMax } = u.scales[scaleKey];
|
const { min: yMin, max: yMax } = u.scales[scaleKey];
|
||||||
|
|
||||||
@@ -21,6 +21,18 @@ export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let { steps, mode } = thresholds;
|
||||||
|
|
||||||
|
if (mode === ThresholdsMode.Percentage) {
|
||||||
|
let [min, max] = getDataRange(u, scaleKey);
|
||||||
|
let range = max - min;
|
||||||
|
|
||||||
|
steps = steps.map((step) => ({
|
||||||
|
...step,
|
||||||
|
value: min + range * (step.value / 100),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function addLines() {
|
function addLines() {
|
||||||
// Thresholds below a transparent threshold is treated like "less than", and line drawn previous threshold
|
// Thresholds below a transparent threshold is treated like "less than", and line drawn previous threshold
|
||||||
let transparentIndex = 0;
|
let transparentIndex = 0;
|
||||||
@@ -40,9 +52,9 @@ export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
|||||||
|
|
||||||
// if we are below a transparent index treat this a less then threshold, use previous thresholds color
|
// if we are below a transparent index treat this a less then threshold, use previous thresholds color
|
||||||
if (transparentIndex >= idx && idx > 0) {
|
if (transparentIndex >= idx && idx > 0) {
|
||||||
color = tinycolor(getColorForTheme(steps[idx - 1].color, theme.v1));
|
color = tinycolor(theme.visualization.getColorByName(steps[idx - 1].color));
|
||||||
} else {
|
} else {
|
||||||
color = tinycolor(getColorForTheme(step.color, theme.v1));
|
color = tinycolor(theme.visualization.getColorByName(step.color));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unless alpha specififed set to default value
|
// Unless alpha specififed set to default value
|
||||||
@@ -50,10 +62,10 @@ export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
|||||||
color.setAlpha(0.7);
|
color.setAlpha(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
let x0 = u.valToPos(xMin!, 'x', true);
|
let x0 = Math.round(u.valToPos(xMin!, 'x', true));
|
||||||
let y0 = u.valToPos(step.value, scaleKey, true);
|
let y0 = Math.round(u.valToPos(step.value, scaleKey, true));
|
||||||
let x1 = u.valToPos(xMax!, 'x', true);
|
let x1 = Math.round(u.valToPos(xMax!, 'x', true));
|
||||||
let y1 = u.valToPos(step.value, scaleKey, true);
|
let y1 = Math.round(u.valToPos(step.value, scaleKey, true));
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
@@ -66,49 +78,26 @@ export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addAreas() {
|
function addAreas() {
|
||||||
for (let idx = 0; idx < steps.length; idx++) {
|
let grd = scaleGradient(
|
||||||
const step = steps[idx];
|
u,
|
||||||
|
u.series[1].scale!,
|
||||||
|
GradientDirection.Up,
|
||||||
|
steps.map((step) => {
|
||||||
|
let color = tinycolor(theme.visualization.getColorByName(step.color));
|
||||||
|
|
||||||
// skip thresholds that cannot be seen
|
if (color.getAlpha() === 1) {
|
||||||
if (step.value > yMax!) {
|
color.setAlpha(0.15);
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if this is the last step make the next step the same color but +Infinity
|
return [step.value, color.toString()];
|
||||||
const nextStep =
|
}),
|
||||||
idx + 1 < steps.length
|
true
|
||||||
? steps[idx + 1]
|
);
|
||||||
: {
|
|
||||||
...step,
|
|
||||||
value: Infinity,
|
|
||||||
};
|
|
||||||
|
|
||||||
let color = tinycolor(getColorForTheme(step.color, theme.v1));
|
ctx.save();
|
||||||
|
ctx.fillStyle = grd;
|
||||||
// Ignore fully transparent colors
|
ctx.fillRect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||||
const alpha = color.getAlpha();
|
ctx.restore();
|
||||||
if (alpha === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// if no alpha set automatic alpha
|
|
||||||
if (alpha === 1) {
|
|
||||||
color = color.setAlpha(0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = step.value === -Infinity ? yMin : step.value;
|
|
||||||
let nextValue = nextStep.value === Infinity || nextStep.value > yMax! ? yMax : nextStep.value;
|
|
||||||
|
|
||||||
let x0 = u.valToPos(xMin ?? 0, 'x', true);
|
|
||||||
let y0 = u.valToPos(value ?? 0, scaleKey, true);
|
|
||||||
let x1 = u.valToPos(xMax ?? 1, 'x', true);
|
|
||||||
let y1 = u.valToPos(nextValue ?? 1, scaleKey, true);
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.fillStyle = color.toString();
|
|
||||||
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.mode) {
|
switch (config.mode) {
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { FieldColorMode, FieldColorModeId, GrafanaTheme2, ThresholdsConfig, ThresholdsMode } from '@grafana/data';
|
import {
|
||||||
|
colorManipulator,
|
||||||
|
FieldColorMode,
|
||||||
|
FieldColorModeId,
|
||||||
|
GrafanaTheme2,
|
||||||
|
ThresholdsConfig,
|
||||||
|
ThresholdsMode,
|
||||||
|
} from '@grafana/data';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
import { getCanvasContext } from '../../../utils/measureText';
|
import { getCanvasContext } from '../../../utils/measureText';
|
||||||
@@ -11,8 +18,8 @@ export function getOpacityGradientFn(
|
|||||||
const ctx = getCanvasContext();
|
const ctx = getCanvasContext();
|
||||||
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
|
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
|
||||||
|
|
||||||
gradient.addColorStop(0, tinycolor(color).setAlpha(opacity).toRgbString());
|
gradient.addColorStop(0, colorManipulator.alpha(color, opacity));
|
||||||
gradient.addColorStop(1, tinycolor(color).setAlpha(0).toRgbString());
|
gradient.addColorStop(1, colorManipulator.alpha(color, 0));
|
||||||
|
|
||||||
return gradient;
|
return gradient;
|
||||||
};
|
};
|
||||||
@@ -39,10 +46,132 @@ export function getHueGradientFn(
|
|||||||
return gradient;
|
return gradient;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Experimental & quick and dirty test
|
export enum GradientDirection {
|
||||||
* Not being used
|
'Right' = 0,
|
||||||
*/
|
'Up' = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueStop = [value: number, color: string];
|
||||||
|
|
||||||
|
type ScaleValueStops = ValueStop[];
|
||||||
|
|
||||||
|
export function scaleGradient(
|
||||||
|
u: uPlot,
|
||||||
|
scaleKey: string,
|
||||||
|
dir: GradientDirection,
|
||||||
|
scaleStops: ScaleValueStops,
|
||||||
|
discrete = false
|
||||||
|
) {
|
||||||
|
let scale = u.scales[scaleKey];
|
||||||
|
|
||||||
|
// we want the stop below or at the scaleMax
|
||||||
|
// and the stop below or at the scaleMin, else the stop above scaleMin
|
||||||
|
let minStopIdx: number | null = null;
|
||||||
|
let maxStopIdx: number | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < scaleStops.length; i++) {
|
||||||
|
let stopVal = scaleStops[i][0];
|
||||||
|
|
||||||
|
if (stopVal <= scale.min! || minStopIdx == null) {
|
||||||
|
minStopIdx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxStopIdx = i;
|
||||||
|
|
||||||
|
if (stopVal >= scale.max!) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minStopIdx === maxStopIdx) {
|
||||||
|
return scaleStops[minStopIdx!][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let minStopVal = scaleStops[minStopIdx!][0];
|
||||||
|
let maxStopVal = scaleStops[maxStopIdx!][0];
|
||||||
|
|
||||||
|
if (minStopVal === -Infinity) {
|
||||||
|
minStopVal = scale.min!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStopVal === Infinity) {
|
||||||
|
maxStopVal = scale.max!;
|
||||||
|
}
|
||||||
|
|
||||||
|
let minStopPos = Math.round(u.valToPos(minStopVal, scaleKey, true));
|
||||||
|
let maxStopPos = Math.round(u.valToPos(maxStopVal, scaleKey, true));
|
||||||
|
|
||||||
|
let range = minStopPos - maxStopPos;
|
||||||
|
|
||||||
|
let x0, y0, x1, y1;
|
||||||
|
|
||||||
|
if (dir === GradientDirection.Up) {
|
||||||
|
x0 = x1 = 0;
|
||||||
|
y0 = minStopPos;
|
||||||
|
y1 = maxStopPos;
|
||||||
|
} else {
|
||||||
|
y0 = y1 = 0;
|
||||||
|
x0 = minStopPos;
|
||||||
|
x1 = maxStopPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = getCanvasContext();
|
||||||
|
|
||||||
|
let grd = ctx.createLinearGradient(x0, y0, x1, y1);
|
||||||
|
|
||||||
|
let prevColor: string;
|
||||||
|
|
||||||
|
for (let i = minStopIdx!; i <= maxStopIdx!; i++) {
|
||||||
|
let s = scaleStops[i];
|
||||||
|
|
||||||
|
let stopPos =
|
||||||
|
i === minStopIdx ? minStopPos : i === maxStopIdx ? maxStopPos : Math.round(u.valToPos(s[0], scaleKey, true));
|
||||||
|
|
||||||
|
let pct = (minStopPos - stopPos) / range;
|
||||||
|
|
||||||
|
if (discrete && i > minStopIdx!) {
|
||||||
|
grd.addColorStop(pct, prevColor!);
|
||||||
|
}
|
||||||
|
|
||||||
|
grd.addColorStop(pct, (prevColor = s[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return grd;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDataRange(plot: uPlot, scaleKey: string) {
|
||||||
|
let sc = plot.scales[scaleKey];
|
||||||
|
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
|
||||||
|
plot.series.forEach((ser, seriesIdx) => {
|
||||||
|
if (ser.show && ser.scale === scaleKey) {
|
||||||
|
// uPlot skips finding data min/max when a scale has a pre-defined range
|
||||||
|
if (ser.min == null) {
|
||||||
|
let data = plot.data[seriesIdx];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (data[i] != null) {
|
||||||
|
min = Math.min(min, data[i]!);
|
||||||
|
max = Math.max(max, data[i]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
min = Math.min(min, ser.min!);
|
||||||
|
max = Math.max(max, ser.max!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
min = sc.min!;
|
||||||
|
max = sc.max!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
export function getScaleGradientFn(
|
export function getScaleGradientFn(
|
||||||
opacity: number,
|
opacity: number,
|
||||||
theme: GrafanaTheme2,
|
theme: GrafanaTheme2,
|
||||||
@@ -58,68 +187,41 @@ export function getScaleGradientFn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (plot: uPlot, seriesIdx: number) => {
|
return (plot: uPlot, seriesIdx: number) => {
|
||||||
// A uplot bug (I think) where this is called before there is bbox
|
let scaleKey = plot.series[seriesIdx].scale!;
|
||||||
// Color used for cursor highlight, not sure what to do here as this is called before we have bbox
|
|
||||||
// and only once so same color is used for all points
|
|
||||||
if (plot.bbox.top == null) {
|
|
||||||
return theme.colors.text.primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = getCanvasContext();
|
let gradient: CanvasGradient | string = '';
|
||||||
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
|
|
||||||
const canvasHeight = plot.bbox.height;
|
|
||||||
const series = plot.series[seriesIdx];
|
|
||||||
const scale = plot.scales[series.scale!];
|
|
||||||
const scaleMin = scale.min ?? 0;
|
|
||||||
const scaleMax = scale.max ?? 100;
|
|
||||||
const scaleRange = scaleMax - scaleMin;
|
|
||||||
|
|
||||||
const addColorStop = (value: number, color: string) => {
|
|
||||||
const pos = plot.valToPos(value, series.scale!, true);
|
|
||||||
// when above range we get negative values here
|
|
||||||
if (pos < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const percent = Math.max(pos / canvasHeight, 0);
|
|
||||||
const realColor = tinycolor(theme.visualization.getColorByName(color)).setAlpha(opacity).toString();
|
|
||||||
const colorStopPos = Math.min(percent, 1);
|
|
||||||
|
|
||||||
gradient.addColorStop(colorStopPos, realColor);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (colorMode.id === FieldColorModeId.Thresholds) {
|
if (colorMode.id === FieldColorModeId.Thresholds) {
|
||||||
for (let idx = 0; idx < thresholds.steps.length; idx++) {
|
if (thresholds.mode === ThresholdsMode.Absolute) {
|
||||||
const step = thresholds.steps[idx];
|
const valueStops = thresholds.steps.map(
|
||||||
|
(step) =>
|
||||||
if (thresholds.mode === ThresholdsMode.Absolute) {
|
[step.value, colorManipulator.alpha(theme.visualization.getColorByName(step.color), opacity)] as ValueStop
|
||||||
const value = step.value === -Infinity ? scaleMin : step.value;
|
);
|
||||||
addColorStop(value, step.color);
|
gradient = scaleGradient(plot, scaleKey, GradientDirection.Up, valueStops, true);
|
||||||
|
} else {
|
||||||
if (thresholds.steps.length > idx + 1) {
|
const [min, max] = getDataRange(plot, scaleKey);
|
||||||
// to make the gradient discrete
|
const range = max - min;
|
||||||
addColorStop(thresholds.steps[idx + 1].value - 0.00000001, step.color);
|
const valueStops = thresholds.steps.map(
|
||||||
}
|
(step) =>
|
||||||
} else {
|
[
|
||||||
const percent = step.value === -Infinity ? 0 : step.value;
|
min + range * (step.value / 100),
|
||||||
const realValue = (percent / 100) * scaleRange;
|
colorManipulator.alpha(theme.visualization.getColorByName(step.color), opacity),
|
||||||
addColorStop(realValue, step.color);
|
] as ValueStop
|
||||||
|
);
|
||||||
// to make the gradient discrete
|
gradient = scaleGradient(plot, scaleKey, GradientDirection.Up, valueStops, true);
|
||||||
if (thresholds.steps.length > idx + 1) {
|
|
||||||
// to make the gradient discrete
|
|
||||||
const nextValue = (thresholds.steps[idx + 1].value / 100) * scaleRange - 0.0000001;
|
|
||||||
addColorStop(nextValue, step.color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (colorMode.getColors) {
|
} else if (colorMode.getColors) {
|
||||||
const colors = colorMode.getColors(theme);
|
const colors = colorMode.getColors(theme);
|
||||||
const stepValue = (scaleMax - scaleMin) / colors.length;
|
const [min, max] = getDataRange(plot, scaleKey);
|
||||||
|
const range = max - min;
|
||||||
for (let idx = 0; idx < colors.length; idx++) {
|
const valueStops = colors.map(
|
||||||
addColorStop(scaleMin + stepValue * idx, colors[idx]);
|
(color, i) =>
|
||||||
}
|
[
|
||||||
|
min + range * (i / (colors.length - 1)),
|
||||||
|
colorManipulator.alpha(theme.visualization.getColorByName(color), opacity),
|
||||||
|
] as ValueStop
|
||||||
|
);
|
||||||
|
gradient = scaleGradient(plot, scaleKey, GradientDirection.Up, valueStops, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gradient;
|
return gradient;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ LineConfig: {
|
|||||||
lineWidth?: number
|
lineWidth?: number
|
||||||
lineInterpolation?: LineInterpolation
|
lineInterpolation?: LineInterpolation
|
||||||
lineStyle?: LineStyle
|
lineStyle?: LineStyle
|
||||||
spanNulls?: bool
|
spanNulls?: bool | number
|
||||||
} @cuetsy(targetType="interface")
|
} @cuetsy(targetType="interface")
|
||||||
|
|
||||||
FillConfig: {
|
FillConfig: {
|
||||||
|
|||||||
@@ -29,15 +29,6 @@ interface TooltipPluginProps {
|
|||||||
renderTooltip?: (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => React.ReactNode;
|
renderTooltip?: (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eqArrays = (a: any[], b: any[]) => {
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
|
||||||
if (a[i] !== b[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TOOLTIP_OFFSET = 10;
|
const TOOLTIP_OFFSET = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,16 +114,13 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
})(u);
|
})(u);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let prevIdx: number | null = null;
|
config.addHook('setLegend', (u) => {
|
||||||
let prevIdxs: Array<number | null> | null = null;
|
setFocusedPointIdx(u.legend.idx!);
|
||||||
|
setFocusedPointIdxs(u.legend.idxs!.slice());
|
||||||
|
});
|
||||||
|
|
||||||
// default series/datapoint idx retireval
|
// default series/datapoint idx retireval
|
||||||
config.addHook('setCursor', (u) => {
|
config.addHook('setCursor', (u) => {
|
||||||
if (u.cursor.idx !== prevIdx || prevIdxs == null || !eqArrays(prevIdxs, u.cursor.idxs!)) {
|
|
||||||
setFocusedPointIdx((prevIdx = u.cursor.idx!));
|
|
||||||
setFocusedPointIdxs((prevIdxs = u.cursor.idxs!.slice()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const bbox = plotCtx.getCanvasBoundingBox();
|
const bbox = plotCtx.getCanvasBoundingBox();
|
||||||
if (!bbox) {
|
if (!bbox) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
|
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
|
||||||
|
<small className={styles.endpointName}> | {label}</small>
|
||||||
</a>
|
</a>
|
||||||
{createSpanLink &&
|
{createSpanLink &&
|
||||||
(() => {
|
(() => {
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.Status != backend.HealthStatusOk {
|
if resp.Status != backend.HealthStatusOk {
|
||||||
return response.JSON(503, payload)
|
return response.JSON(400, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(200, payload)
|
return response.JSON(200, payload)
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ func runPluginCommand(command func(commandLine utils.CommandLine) error) func(co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runCueCommand(command func(commandLine utils.CommandLine) error) func(context *cli.Context) error {
|
||||||
|
return func(context *cli.Context) error {
|
||||||
|
return command(&utils.ContextCommandLine{Context: context})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Command contains command state.
|
// Command contains command state.
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Client utils.ApiClient
|
Client utils.ApiClient
|
||||||
@@ -138,7 +144,7 @@ var cueCommands = []*cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "validate-schema",
|
Name: "validate-schema",
|
||||||
Usage: "validate known *.cue files in the Grafana project",
|
Usage: "validate known *.cue files in the Grafana project",
|
||||||
Action: runPluginCommand(cmd.validateScuemata),
|
Action: runCueCommand(cmd.validateScuemata),
|
||||||
Description: `validate-schema checks that all CUE schema files are valid with respect
|
Description: `validate-schema checks that all CUE schema files are valid with respect
|
||||||
to basic standards - valid CUE, valid scuemata, etc. Note that this
|
to basic standards - valid CUE, valid scuemata, etc. Note that this
|
||||||
command checks only paths that existed when grafana-cli was compiled,
|
command checks only paths that existed when grafana-cli was compiled,
|
||||||
@@ -153,7 +159,7 @@ so must be recompiled to validate newly-added CUE files.`,
|
|||||||
{
|
{
|
||||||
Name: "validate-resource",
|
Name: "validate-resource",
|
||||||
Usage: "validate resource files (e.g. dashboard JSON) against schema",
|
Usage: "validate resource files (e.g. dashboard JSON) against schema",
|
||||||
Action: runPluginCommand(cmd.validateResources),
|
Action: runCueCommand(cmd.validateResources),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "dashboard",
|
Name: "dashboard",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
_ "github.com/grpc-ecosystem/go-grpc-middleware"
|
_ "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
_ "github.com/jung-kurt/gofpdf"
|
_ "github.com/jung-kurt/gofpdf"
|
||||||
_ "github.com/linkedin/goavro/v2"
|
_ "github.com/linkedin/goavro/v2"
|
||||||
|
_ "github.com/m3db/prometheus_remote_client_golang/promremote"
|
||||||
_ "github.com/pkg/errors"
|
_ "github.com/pkg/errors"
|
||||||
_ "github.com/robfig/cron"
|
_ "github.com/robfig/cron"
|
||||||
_ "github.com/robfig/cron/v3"
|
_ "github.com/robfig/cron/v3"
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ func Gziper() func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
for _, pathMatcher := range gzipIgnoredPaths {
|
for _, pathMatcher := range gzipIgnoredPaths {
|
||||||
if pathMatcher(requestPath) {
|
if pathMatcher(requestPath) {
|
||||||
fmt.Println("skip path", requestPath)
|
|
||||||
next.ServeHTTP(rw, req)
|
next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,6 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
|||||||
}
|
}
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"channel": sn.recipient,
|
"channel": sn.recipient,
|
||||||
"text": evalContext.GetNotificationTitle(),
|
|
||||||
"attachments": []map[string]interface{}{
|
"attachments": []map[string]interface{}{
|
||||||
attachment,
|
attachment,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
@@ -20,10 +21,17 @@ import (
|
|||||||
// timeNow makes it possible to test usage of time
|
// timeNow makes it possible to test usage of time
|
||||||
var timeNow = time.Now
|
var timeNow = time.Now
|
||||||
|
|
||||||
|
type Scheduler interface {
|
||||||
|
AlertmanagersFor(orgID int64) []*url.URL
|
||||||
|
DroppedAlertmanagersFor(orgID int64) []*url.URL
|
||||||
|
}
|
||||||
|
|
||||||
type Alertmanager interface {
|
type Alertmanager interface {
|
||||||
// Configuration
|
// Configuration
|
||||||
SaveAndApplyConfig(config *apimodels.PostableUserConfig) error
|
// temporary add orgID parameter; this will move to the Alertmanager wrapper when it will be available
|
||||||
SaveAndApplyDefaultConfig() error
|
SaveAndApplyConfig(orgID int64, config *apimodels.PostableUserConfig) error
|
||||||
|
// temporary add orgID parameter; this will move to the Alertmanager wrapper when it will be available
|
||||||
|
SaveAndApplyDefaultConfig(orgID int64) error
|
||||||
GetStatus() apimodels.GettableStatus
|
GetStatus() apimodels.GettableStatus
|
||||||
|
|
||||||
// Silences
|
// Silences
|
||||||
@@ -61,19 +69,19 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) {
|
|||||||
DataProxy: api.DataProxy,
|
DataProxy: api.DataProxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register endpoints for proxing to Alertmanager-compatible backends.
|
// Register endpoints for proxying to Alertmanager-compatible backends.
|
||||||
api.RegisterAlertmanagerApiEndpoints(NewForkedAM(
|
api.RegisterAlertmanagerApiEndpoints(NewForkedAM(
|
||||||
api.DatasourceCache,
|
api.DatasourceCache,
|
||||||
NewLotexAM(proxy, logger),
|
NewLotexAM(proxy, logger),
|
||||||
AlertmanagerSrv{store: api.AlertingStore, am: api.Alertmanager, log: logger},
|
AlertmanagerSrv{store: api.AlertingStore, am: api.Alertmanager, log: logger},
|
||||||
), m)
|
), m)
|
||||||
// Register endpoints for proxing to Prometheus-compatible backends.
|
// Register endpoints for proxying to Prometheus-compatible backends.
|
||||||
api.RegisterPrometheusApiEndpoints(NewForkedProm(
|
api.RegisterPrometheusApiEndpoints(NewForkedProm(
|
||||||
api.DatasourceCache,
|
api.DatasourceCache,
|
||||||
NewLotexProm(proxy, logger),
|
NewLotexProm(proxy, logger),
|
||||||
PrometheusSrv{log: logger, manager: api.StateManager, store: api.RuleStore},
|
PrometheusSrv{log: logger, manager: api.StateManager, store: api.RuleStore},
|
||||||
), m)
|
), m)
|
||||||
// Register endpoints for proxing to Cortex Ruler-compatible backends.
|
// Register endpoints for proxying to Cortex Ruler-compatible backends.
|
||||||
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
||||||
api.DatasourceCache,
|
api.DatasourceCache,
|
||||||
NewLotexRuler(proxy, logger),
|
NewLotexRuler(proxy, logger),
|
||||||
@@ -87,7 +95,8 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) {
|
|||||||
log: logger,
|
log: logger,
|
||||||
}, m)
|
}, m)
|
||||||
api.RegisterConfigurationApiEndpoints(AdminSrv{
|
api.RegisterConfigurationApiEndpoints(AdminSrv{
|
||||||
store: api.AdminConfigStore,
|
store: api.AdminConfigStore,
|
||||||
log: logger,
|
log: logger,
|
||||||
|
scheduler: api.Schedule,
|
||||||
}, m)
|
}, m)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,32 @@ import (
|
|||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
|
||||||
|
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminSrv struct {
|
type AdminSrv struct {
|
||||||
store store.AdminConfigurationStore
|
scheduler Scheduler
|
||||||
log log.Logger
|
store store.AdminConfigurationStore
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv AdminSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response {
|
||||||
|
urls := srv.scheduler.AlertmanagersFor(c.OrgId)
|
||||||
|
droppedURLs := srv.scheduler.DroppedAlertmanagersFor(c.OrgId)
|
||||||
|
ams := v1.AlertManagersResult{Active: make([]v1.AlertManager, len(urls)), Dropped: make([]v1.AlertManager, len(droppedURLs))}
|
||||||
|
for i, url := range urls {
|
||||||
|
ams.Active[i].URL = url.String()
|
||||||
|
}
|
||||||
|
for i, url := range droppedURLs {
|
||||||
|
ams.Dropped[i].URL = url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(http.StatusOK, apimodels.GettableAlertmanagers{
|
||||||
|
Status: "success",
|
||||||
|
Data: ams,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv AdminSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response {
|
func (srv AdminSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response {
|
||||||
@@ -56,7 +77,7 @@ func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.
|
|||||||
return ErrResp(http.StatusBadRequest, err, msg)
|
return ErrResp(http.StatusBadRequest, err, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(http.StatusCreated, "admin configuration updated")
|
return response.JSON(http.StatusCreated, util.DynMap{"message": "admin configuration updated"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv AdminSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response {
|
func (srv AdminSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response {
|
||||||
@@ -70,5 +91,5 @@ func (srv AdminSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Resp
|
|||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(http.StatusOK, "admin configuration deleted")
|
return response.JSON(http.StatusOK, util.DynMap{"message": "admin configuration deleted"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) respo
|
|||||||
if !c.HasUserRole(models.ROLE_EDITOR) {
|
if !c.HasUserRole(models.ROLE_EDITOR) {
|
||||||
return ErrResp(http.StatusForbidden, errors.New("permission denied"), "")
|
return ErrResp(http.StatusForbidden, errors.New("permission denied"), "")
|
||||||
}
|
}
|
||||||
if err := srv.am.SaveAndApplyDefaultConfig(); err != nil {
|
if err := srv.am.SaveAndApplyDefaultConfig(c.OrgId); err != nil {
|
||||||
srv.log.Error("unable to save and apply default alertmanager configuration", "err", err)
|
srv.log.Error("unable to save and apply default alertmanager configuration", "err", err)
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to save and apply default Alertmanager configuration")
|
return ErrResp(http.StatusInternalServerError, err, "failed to save and apply default Alertmanager configuration")
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,8 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response
|
|||||||
if !c.HasUserRole(models.ROLE_EDITOR) {
|
if !c.HasUserRole(models.ROLE_EDITOR) {
|
||||||
return ErrResp(http.StatusForbidden, errors.New("permission denied"), "")
|
return ErrResp(http.StatusForbidden, errors.New("permission denied"), "")
|
||||||
}
|
}
|
||||||
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{}
|
|
||||||
|
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: c.OrgId}
|
||||||
if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil {
|
if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil {
|
||||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
@@ -201,7 +202,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the last known working configuration
|
// Get the last known working configuration
|
||||||
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{}
|
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: c.OrgId}
|
||||||
if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil {
|
if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil {
|
||||||
// If we don't have a configuration there's nothing for us to know and we should just continue saving the new one
|
// If we don't have a configuration there's nothing for us to know and we should just continue saving the new one
|
||||||
if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||||
@@ -255,7 +256,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap
|
|||||||
return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration")
|
return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := srv.am.SaveAndApplyConfig(&body); err != nil {
|
if err := srv.am.SaveAndApplyConfig(c.OrgId, &body); err != nil {
|
||||||
srv.log.Error("unable to save and apply alertmanager configuration", "err", err)
|
srv.log.Error("unable to save and apply alertmanager configuration", "err", err)
|
||||||
return ErrResp(http.StatusBadRequest, err, "failed to save and apply Alertmanager configuration")
|
return ErrResp(http.StatusBadRequest, err, "failed to save and apply Alertmanager configuration")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
type ConfigurationApiService interface {
|
type ConfigurationApiService interface {
|
||||||
RouteDeleteNGalertConfig(*models.ReqContext) response.Response
|
RouteDeleteNGalertConfig(*models.ReqContext) response.Response
|
||||||
|
RouteGetAlertmanagers(*models.ReqContext) response.Response
|
||||||
RouteGetNGalertConfig(*models.ReqContext) response.Response
|
RouteGetNGalertConfig(*models.ReqContext) response.Response
|
||||||
RoutePostNGalertConfig(*models.ReqContext, apimodels.PostableNGalertConfig) response.Response
|
RoutePostNGalertConfig(*models.ReqContext, apimodels.PostableNGalertConfig) response.Response
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,15 @@ func (api *API) RegisterConfigurationApiEndpoints(srv ConfigurationApiService, m
|
|||||||
m,
|
m,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
group.Get(
|
||||||
|
toMacaronPath("/api/v1/ngalert/alertmanagers"),
|
||||||
|
metrics.Instrument(
|
||||||
|
http.MethodGet,
|
||||||
|
"/api/v1/ngalert/alertmanagers",
|
||||||
|
srv.RouteGetAlertmanagers,
|
||||||
|
m,
|
||||||
|
),
|
||||||
|
)
|
||||||
group.Get(
|
group.Get(
|
||||||
toMacaronPath("/api/v1/ngalert/admin_config"),
|
toMacaronPath("/api/v1/ngalert/admin_config"),
|
||||||
metrics.Instrument(
|
metrics.Instrument(
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ spec.json: $(GO_PKG_FILES)
|
|||||||
quay.io/goswagger/swagger:$(SWAGGER_TAG) \
|
quay.io/goswagger/swagger:$(SWAGGER_TAG) \
|
||||||
generate spec -m -o $@
|
generate spec -m -o $@
|
||||||
|
|
||||||
|
ensure_go-swagger_mac:
|
||||||
|
@hash swagger &>/dev/null || (brew tap go-swagger/go-swagger && brew install go-swagger)
|
||||||
|
|
||||||
|
spec.json-mac: ensure_go-swagger_mac $(GO_PKG_FILES)
|
||||||
|
swagger generate spec -m -w $(API_DIR) -o spec.json
|
||||||
|
|
||||||
post.json: spec.json
|
post.json: spec.json
|
||||||
go run cmd/clean-swagger/main.go -if $(<) -of $@
|
go run cmd/clean-swagger/main.go -if $(<) -of $@
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
package definitions
|
package definitions
|
||||||
|
|
||||||
|
import v1 "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||||
|
|
||||||
|
// swagger:route GET /api/v1/ngalert/alertmanagers configuration RouteGetAlertmanagers
|
||||||
|
//
|
||||||
|
// Get the discovered and dropped Alertmanagers of the user's organization based on the specified configuration.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: GettableAlertmanagers
|
||||||
|
|
||||||
// swagger:route GET /api/v1/ngalert/admin_config configuration RouteGetNGalertConfig
|
// swagger:route GET /api/v1/ngalert/admin_config configuration RouteGetNGalertConfig
|
||||||
//
|
//
|
||||||
// Get the NGalert configuration of the user's organization, returns 404 if no configuration is present.
|
// Get the NGalert configuration of the user's organization, returns 404 if no configuration is present.
|
||||||
@@ -49,3 +61,9 @@ type PostableNGalertConfig struct {
|
|||||||
type GettableNGalertConfig struct {
|
type GettableNGalertConfig struct {
|
||||||
Alertmanagers []string `json:"alertmanagers"`
|
Alertmanagers []string `json:"alertmanagers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:model
|
||||||
|
type GettableAlertmanagers struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data v1.AlertManagersResult `json:"data"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import (
|
|||||||
// get alertmanager alerts
|
// get alertmanager alerts
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: GettableAlerts
|
// 200: gettableAlerts
|
||||||
// 400: ValidationError
|
// 400: ValidationError
|
||||||
|
|
||||||
// swagger:route POST /api/alertmanager/{Recipient}/api/v2/alerts alertmanager RoutePostAMAlerts
|
// swagger:route POST /api/alertmanager/{Recipient}/api/v2/alerts alertmanager RoutePostAMAlerts
|
||||||
@@ -70,7 +70,7 @@ import (
|
|||||||
// get alertmanager alerts
|
// get alertmanager alerts
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: AlertGroups
|
// 200: alertGroups
|
||||||
// 400: ValidationError
|
// 400: ValidationError
|
||||||
|
|
||||||
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/silences alertmanager RouteGetSilences
|
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/silences alertmanager RouteGetSilences
|
||||||
@@ -78,7 +78,7 @@ import (
|
|||||||
// get silences
|
// get silences
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: GettableSilences
|
// 200: gettableSilences
|
||||||
// 400: ValidationError
|
// 400: ValidationError
|
||||||
|
|
||||||
// swagger:route POST /api/alertmanager/{Recipient}/api/v2/silences alertmanager RouteCreateSilence
|
// swagger:route POST /api/alertmanager/{Recipient}/api/v2/silences alertmanager RouteCreateSilence
|
||||||
@@ -86,7 +86,7 @@ import (
|
|||||||
// create silence
|
// create silence
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 201: GettableSilence
|
// 201: gettableSilence
|
||||||
// 400: ValidationError
|
// 400: ValidationError
|
||||||
|
|
||||||
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/silence/{SilenceId} alertmanager RouteGetSilence
|
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/silence/{SilenceId} alertmanager RouteGetSilence
|
||||||
@@ -94,7 +94,7 @@ import (
|
|||||||
// get silence
|
// get silence
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: GettableSilence
|
// 200: gettableSilence
|
||||||
// 400: ValidationError
|
// 400: ValidationError
|
||||||
|
|
||||||
// swagger:route DELETE /api/alertmanager/{Recipient}/api/v2/silence/{SilenceId} alertmanager RouteDeleteSilence
|
// swagger:route DELETE /api/alertmanager/{Recipient}/api/v2/silence/{SilenceId} alertmanager RouteDeleteSilence
|
||||||
@@ -196,28 +196,28 @@ func NewGettableStatus(cfg *PostableApiAlertingConfig) *GettableStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model postableSilence
|
||||||
type PostableSilence = amv2.PostableSilence
|
type PostableSilence = amv2.PostableSilence
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model gettableSilences
|
||||||
type GettableSilences = amv2.GettableSilences
|
type GettableSilences = amv2.GettableSilences
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model gettableSilence
|
||||||
type GettableSilence = amv2.GettableSilence
|
type GettableSilence = amv2.GettableSilence
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model gettableAlerts
|
||||||
type GettableAlerts = amv2.GettableAlerts
|
type GettableAlerts = amv2.GettableAlerts
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model gettableAlert
|
||||||
type GettableAlert = amv2.GettableAlert
|
type GettableAlert = amv2.GettableAlert
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model alertGroups
|
||||||
type AlertGroups = amv2.AlertGroups
|
type AlertGroups = amv2.AlertGroups
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model alertGroup
|
||||||
type AlertGroup = amv2.AlertGroup
|
type AlertGroup = amv2.AlertGroup
|
||||||
|
|
||||||
// swagger:model
|
// swagger:model receiver
|
||||||
type Receiver = amv2.Receiver
|
type Receiver = amv2.Receiver
|
||||||
|
|
||||||
// swagger:parameters RouteGetAMAlerts RouteGetAMAlertGroups
|
// swagger:parameters RouteGetAMAlerts RouteGetAMAlertGroups
|
||||||
|
|||||||
@@ -57,12 +57,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"AlertGroup": {
|
|
||||||
"$ref": "#/definitions/alertGroup"
|
|
||||||
},
|
|
||||||
"AlertGroups": {
|
|
||||||
"$ref": "#/definitions/alertGroups"
|
|
||||||
},
|
|
||||||
"AlertInstancesResponse": {
|
"AlertInstancesResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"instances": {
|
"instances": {
|
||||||
@@ -81,6 +75,38 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
|
"AlertManager": {
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AlertManager models a configured Alert Manager.",
|
||||||
|
"type": "object",
|
||||||
|
"x-go-package": "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||||
|
},
|
||||||
|
"AlertManagersResult": {
|
||||||
|
"properties": {
|
||||||
|
"activeAlertManagers": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AlertManager"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"x-go-name": "Active"
|
||||||
|
},
|
||||||
|
"droppedAlertManagers": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AlertManager"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"x-go-name": "Dropped"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AlertManagersResult contains the result from querying the alertmanagers endpoint.",
|
||||||
|
"type": "object",
|
||||||
|
"x-go-package": "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||||
|
},
|
||||||
"AlertQuery": {
|
"AlertQuery": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"datasourceUid": {
|
"datasourceUid": {
|
||||||
@@ -477,11 +503,18 @@
|
|||||||
"Failure": {
|
"Failure": {
|
||||||
"$ref": "#/definitions/ResponseDetails"
|
"$ref": "#/definitions/ResponseDetails"
|
||||||
},
|
},
|
||||||
"GettableAlert": {
|
"GettableAlertmanagers": {
|
||||||
"$ref": "#/definitions/gettableAlert"
|
"properties": {
|
||||||
},
|
"data": {
|
||||||
"GettableAlerts": {
|
"$ref": "#/definitions/AlertManagersResult"
|
||||||
"$ref": "#/definitions/gettableAlerts"
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Status"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"GettableApiAlertingConfig": {
|
"GettableApiAlertingConfig": {
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -786,8 +819,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"GettableSilence": {},
|
|
||||||
"GettableSilences": {},
|
|
||||||
"GettableStatus": {
|
"GettableStatus": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"cluster": {
|
"cluster": {
|
||||||
@@ -1589,7 +1620,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"PostableSilence": {},
|
|
||||||
"PostableUserConfig": {
|
"PostableUserConfig": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"alertmanager_config": {
|
"alertmanager_config": {
|
||||||
@@ -1724,7 +1754,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Receiver configuration provides configuration on how to contact a receiver.",
|
"title": "Receiver configuration provides configuration on how to contact a receiver.",
|
||||||
"type": "object"
|
"type": "object",
|
||||||
|
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||||
},
|
},
|
||||||
"Regexp": {
|
"Regexp": {
|
||||||
"description": "A Regexp is safe for concurrent use by multiple goroutines,\nexcept for configuration methods, such as Longest.",
|
"description": "A Regexp is safe for concurrent use by multiple goroutines,\nexcept for configuration methods, such as Longest.",
|
||||||
@@ -2458,7 +2489,7 @@
|
|||||||
"$ref": "#/definitions/labelSet"
|
"$ref": "#/definitions/labelSet"
|
||||||
},
|
},
|
||||||
"receiver": {
|
"receiver": {
|
||||||
"$ref": "#/definitions/Receiver"
|
"$ref": "#/definitions/receiver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2466,18 +2497,14 @@
|
|||||||
"labels",
|
"labels",
|
||||||
"receiver"
|
"receiver"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object"
|
||||||
"x-go-name": "AlertGroup",
|
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
},
|
},
|
||||||
"alertGroups": {
|
"alertGroups": {
|
||||||
"description": "AlertGroups alert groups",
|
"description": "AlertGroups alert groups",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/alertGroup"
|
"$ref": "#/definitions/alertGroup"
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array"
|
||||||
"x-go-name": "AlertGroups",
|
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
},
|
},
|
||||||
"alertStatus": {
|
"alertStatus": {
|
||||||
"description": "AlertStatus alert status",
|
"description": "AlertStatus alert status",
|
||||||
@@ -2597,7 +2624,6 @@
|
|||||||
"$ref": "#/definitions/Duration"
|
"$ref": "#/definitions/Duration"
|
||||||
},
|
},
|
||||||
"gettableAlert": {
|
"gettableAlert": {
|
||||||
"description": "GettableAlert gettable alert",
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"$ref": "#/definitions/labelSet"
|
"$ref": "#/definitions/labelSet"
|
||||||
@@ -2625,7 +2651,7 @@
|
|||||||
"receivers": {
|
"receivers": {
|
||||||
"description": "receivers",
|
"description": "receivers",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Receiver"
|
"$ref": "#/definitions/receiver"
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"x-go-name": "Receivers"
|
"x-go-name": "Receivers"
|
||||||
@@ -2661,7 +2687,6 @@
|
|||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||||
},
|
},
|
||||||
"gettableAlerts": {
|
"gettableAlerts": {
|
||||||
"description": "GettableAlerts gettable alerts",
|
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/gettableAlert"
|
"$ref": "#/definitions/gettableAlert"
|
||||||
},
|
},
|
||||||
@@ -2722,14 +2747,11 @@
|
|||||||
"status",
|
"status",
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object"
|
||||||
"x-go-name": "GettableSilence",
|
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
},
|
},
|
||||||
"gettableSilences": {
|
"gettableSilences": {
|
||||||
"description": "GettableSilences gettable silences",
|
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/GettableSilence"
|
"$ref": "#/definitions/gettableSilence"
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"x-go-name": "GettableSilences",
|
"x-go-name": "GettableSilences",
|
||||||
@@ -2862,7 +2884,6 @@
|
|||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||||
},
|
},
|
||||||
"postableSilence": {
|
"postableSilence": {
|
||||||
"description": "PostableSilence postable silence",
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"comment": {
|
"comment": {
|
||||||
"description": "comment",
|
"description": "comment",
|
||||||
@@ -2907,7 +2928,6 @@
|
|||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||||
},
|
},
|
||||||
"receiver": {
|
"receiver": {
|
||||||
"description": "Receiver receiver",
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"description": "name",
|
"description": "name",
|
||||||
@@ -3090,9 +3110,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "GettableAlerts",
|
"description": "gettableAlerts",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableAlerts"
|
"$ref": "#/definitions/gettableAlerts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -3203,9 +3223,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "AlertGroups",
|
"description": "alertGroups",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/AlertGroups"
|
"$ref": "#/definitions/alertGroups"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -3277,9 +3297,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "GettableSilence",
|
"description": "gettableSilence",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableSilence"
|
"$ref": "#/definitions/gettableSilence"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -3318,9 +3338,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "GettableSilences",
|
"description": "gettableSilences",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableSilences"
|
"$ref": "#/definitions/gettableSilences"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -3355,9 +3375,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"201": {
|
||||||
"description": "GettableSilence",
|
"description": "gettableSilence",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableSilence"
|
"$ref": "#/definitions/gettableSilence"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -3888,6 +3908,26 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/ngalert/alertmanagers": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "RouteGetAlertmanagers",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "GettableAlertmanagers",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/GettableAlertmanagers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Get the discovered and dropped Alertmanagers of the user's organization based on the specified configuration.",
|
||||||
|
"tags": [
|
||||||
|
"configuration"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/receiver/test/{Recipient}": {
|
"/api/v1/receiver/test/{Recipient}": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|||||||
@@ -75,9 +75,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "GettableAlerts",
|
"description": "gettableAlerts",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableAlerts"
|
"$ref": "#/definitions/gettableAlerts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -188,9 +188,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "AlertGroups",
|
"description": "alertGroups",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/AlertGroups"
|
"$ref": "#/definitions/alertGroups"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -226,9 +226,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "GettableSilence",
|
"description": "gettableSilence",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableSilence"
|
"$ref": "#/definitions/gettableSilence"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -303,9 +303,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "GettableSilences",
|
"description": "gettableSilences",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableSilences"
|
"$ref": "#/definitions/gettableSilences"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -340,9 +340,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"201": {
|
||||||
"description": "GettableSilence",
|
"description": "gettableSilence",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/GettableSilence"
|
"$ref": "#/definitions/gettableSilence"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -870,6 +870,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/ngalert/alertmanagers": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"configuration"
|
||||||
|
],
|
||||||
|
"summary": "Get the discovered and dropped Alertmanagers of the user's organization based on the specified configuration.",
|
||||||
|
"operationId": "RouteGetAlertmanagers",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "GettableAlertmanagers",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/GettableAlertmanagers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/receiver/test/{Recipient}": {
|
"/api/v1/receiver/test/{Recipient}": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Test receiver",
|
"description": "Test receiver",
|
||||||
@@ -1015,12 +1035,6 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"AlertGroup": {
|
|
||||||
"$ref": "#/definitions/alertGroup"
|
|
||||||
},
|
|
||||||
"AlertGroups": {
|
|
||||||
"$ref": "#/definitions/alertGroups"
|
|
||||||
},
|
|
||||||
"AlertInstancesResponse": {
|
"AlertInstancesResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1039,6 +1053,38 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
|
"AlertManager": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "AlertManager models a configured Alert Manager.",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||||
|
},
|
||||||
|
"AlertManagersResult": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "AlertManagersResult contains the result from querying the alertmanagers endpoint.",
|
||||||
|
"properties": {
|
||||||
|
"activeAlertManagers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AlertManager"
|
||||||
|
},
|
||||||
|
"x-go-name": "Active"
|
||||||
|
},
|
||||||
|
"droppedAlertManagers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AlertManager"
|
||||||
|
},
|
||||||
|
"x-go-name": "Dropped"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||||
|
},
|
||||||
"AlertQuery": {
|
"AlertQuery": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "AlertQuery represents a single query associated with an alert definition.",
|
"title": "AlertQuery represents a single query associated with an alert definition.",
|
||||||
@@ -1438,11 +1484,18 @@
|
|||||||
"Failure": {
|
"Failure": {
|
||||||
"$ref": "#/definitions/ResponseDetails"
|
"$ref": "#/definitions/ResponseDetails"
|
||||||
},
|
},
|
||||||
"GettableAlert": {
|
"GettableAlertmanagers": {
|
||||||
"$ref": "#/definitions/gettableAlert"
|
"type": "object",
|
||||||
},
|
"properties": {
|
||||||
"GettableAlerts": {
|
"data": {
|
||||||
"$ref": "#/definitions/gettableAlerts"
|
"$ref": "#/definitions/AlertManagersResult"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Status"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"GettableApiAlertingConfig": {
|
"GettableApiAlertingConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1747,12 +1800,6 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"GettableSilence": {
|
|
||||||
"$ref": "#/definitions/GettableSilence"
|
|
||||||
},
|
|
||||||
"GettableSilences": {
|
|
||||||
"$ref": "#/definitions/GettableSilences"
|
|
||||||
},
|
|
||||||
"GettableStatus": {
|
"GettableStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2555,9 +2602,6 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
},
|
},
|
||||||
"PostableSilence": {
|
|
||||||
"$ref": "#/definitions/PostableSilence"
|
|
||||||
},
|
|
||||||
"PostableUserConfig": {
|
"PostableUserConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2693,7 +2737,7 @@
|
|||||||
"x-go-name": "WechatConfigs"
|
"x-go-name": "WechatConfigs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$ref": "#/definitions/Receiver"
|
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||||
},
|
},
|
||||||
"Regexp": {
|
"Regexp": {
|
||||||
"description": "A Regexp is safe for concurrent use by multiple goroutines,\nexcept for configuration methods, such as Longest.",
|
"description": "A Regexp is safe for concurrent use by multiple goroutines,\nexcept for configuration methods, such as Longest.",
|
||||||
@@ -3433,11 +3477,10 @@
|
|||||||
"$ref": "#/definitions/labelSet"
|
"$ref": "#/definitions/labelSet"
|
||||||
},
|
},
|
||||||
"receiver": {
|
"receiver": {
|
||||||
"$ref": "#/definitions/Receiver"
|
"$ref": "#/definitions/receiver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-name": "AlertGroup",
|
"$ref": "#/definitions/alertGroup"
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
},
|
},
|
||||||
"alertGroups": {
|
"alertGroups": {
|
||||||
"description": "AlertGroups alert groups",
|
"description": "AlertGroups alert groups",
|
||||||
@@ -3445,8 +3488,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/alertGroup"
|
"$ref": "#/definitions/alertGroup"
|
||||||
},
|
},
|
||||||
"x-go-name": "AlertGroups",
|
"$ref": "#/definitions/alertGroups"
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
},
|
},
|
||||||
"alertStatus": {
|
"alertStatus": {
|
||||||
"description": "AlertStatus alert status",
|
"description": "AlertStatus alert status",
|
||||||
@@ -3566,7 +3608,6 @@
|
|||||||
"$ref": "#/definitions/Duration"
|
"$ref": "#/definitions/Duration"
|
||||||
},
|
},
|
||||||
"gettableAlert": {
|
"gettableAlert": {
|
||||||
"description": "GettableAlert gettable alert",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"labels",
|
"labels",
|
||||||
@@ -3606,7 +3647,7 @@
|
|||||||
"description": "receivers",
|
"description": "receivers",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Receiver"
|
"$ref": "#/definitions/receiver"
|
||||||
},
|
},
|
||||||
"x-go-name": "Receivers"
|
"x-go-name": "Receivers"
|
||||||
},
|
},
|
||||||
@@ -3627,16 +3668,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-name": "GettableAlert",
|
"x-go-name": "GettableAlert",
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||||
|
"$ref": "#/definitions/gettableAlert"
|
||||||
},
|
},
|
||||||
"gettableAlerts": {
|
"gettableAlerts": {
|
||||||
"description": "GettableAlerts gettable alerts",
|
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/gettableAlert"
|
"$ref": "#/definitions/gettableAlert"
|
||||||
},
|
},
|
||||||
"x-go-name": "GettableAlerts",
|
"x-go-name": "GettableAlerts",
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||||
|
"$ref": "#/definitions/gettableAlerts"
|
||||||
},
|
},
|
||||||
"gettableSilence": {
|
"gettableSilence": {
|
||||||
"description": "GettableSilence gettable silence",
|
"description": "GettableSilence gettable silence",
|
||||||
@@ -3692,17 +3734,16 @@
|
|||||||
"x-go-name": "UpdatedAt"
|
"x-go-name": "UpdatedAt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-name": "GettableSilence",
|
"$ref": "#/definitions/gettableSilence"
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
},
|
},
|
||||||
"gettableSilences": {
|
"gettableSilences": {
|
||||||
"description": "GettableSilences gettable silences",
|
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/GettableSilence"
|
"$ref": "#/definitions/gettableSilence"
|
||||||
},
|
},
|
||||||
"x-go-name": "GettableSilences",
|
"x-go-name": "GettableSilences",
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||||
|
"$ref": "#/definitions/gettableSilences"
|
||||||
},
|
},
|
||||||
"labelSet": {
|
"labelSet": {
|
||||||
"description": "LabelSet label set",
|
"description": "LabelSet label set",
|
||||||
@@ -3831,7 +3872,6 @@
|
|||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||||
},
|
},
|
||||||
"postableSilence": {
|
"postableSilence": {
|
||||||
"description": "PostableSilence postable silence",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"comment",
|
"comment",
|
||||||
@@ -3873,10 +3913,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-name": "PostableSilence",
|
"x-go-name": "PostableSilence",
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||||
|
"$ref": "#/definitions/postableSilence"
|
||||||
},
|
},
|
||||||
"receiver": {
|
"receiver": {
|
||||||
"description": "Receiver receiver",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"name"
|
"name"
|
||||||
@@ -3889,7 +3929,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-name": "Receiver",
|
"x-go-name": "Receiver",
|
||||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||||
|
"$ref": "#/definitions/receiver"
|
||||||
},
|
},
|
||||||
"silence": {
|
"silence": {
|
||||||
"description": "Silence silence",
|
"description": "Silence silence",
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ type AlertConfiguration struct {
|
|||||||
ConfigurationVersion string
|
ConfigurationVersion string
|
||||||
CreatedAt int64 `xorm:"created"`
|
CreatedAt int64 `xorm:"created"`
|
||||||
Default bool
|
Default bool
|
||||||
|
OrgID int64 `xorm:"org_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestAlertmanagerConfigurationQuery is the query to get the latest alertmanager configuration.
|
// GetLatestAlertmanagerConfigurationQuery is the query to get the latest alertmanager configuration.
|
||||||
type GetLatestAlertmanagerConfigurationQuery struct {
|
type GetLatestAlertmanagerConfigurationQuery struct {
|
||||||
|
OrgID int64
|
||||||
Result *AlertConfiguration
|
Result *AlertConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +24,5 @@ type SaveAlertmanagerConfigurationCmd struct {
|
|||||||
AlertmanagerConfiguration string
|
AlertmanagerConfiguration string
|
||||||
ConfigurationVersion string
|
ConfigurationVersion string
|
||||||
Default bool
|
Default bool
|
||||||
}
|
OrgID int64
|
||||||
|
|
||||||
type DeleteAlertmanagerConfigurationCmd struct {
|
|
||||||
ID int64
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,17 +81,19 @@ func (ng *AlertNG) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schedCfg := schedule.SchedulerCfg{
|
schedCfg := schedule.SchedulerCfg{
|
||||||
C: clock.New(),
|
C: clock.New(),
|
||||||
BaseInterval: baseInterval,
|
BaseInterval: baseInterval,
|
||||||
Logger: log.New("ngalert.scheduler"),
|
Logger: log.New("ngalert.scheduler"),
|
||||||
MaxAttempts: maxAttempts,
|
MaxAttempts: maxAttempts,
|
||||||
Evaluator: eval.Evaluator{Cfg: ng.Cfg, Log: ng.Log},
|
Evaluator: eval.Evaluator{Cfg: ng.Cfg, Log: ng.Log},
|
||||||
InstanceStore: store,
|
InstanceStore: store,
|
||||||
RuleStore: store,
|
RuleStore: store,
|
||||||
AdminConfigStore: store,
|
AdminConfigStore: store,
|
||||||
Notifier: ng.Alertmanager,
|
Notifier: ng.Alertmanager,
|
||||||
Metrics: ng.Metrics,
|
Metrics: ng.Metrics,
|
||||||
|
AdminConfigPollInterval: ng.Cfg.AdminConfigPollInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
ng.stateManager = state.NewManager(ng.Log, ng.Metrics, store, store)
|
ng.stateManager = state.NewManager(ng.Log, ng.Metrics, store, store)
|
||||||
ng.schedule = schedule.NewScheduler(schedCfg, ng.DataService, ng.Cfg.AppURL, ng.stateManager)
|
ng.schedule = schedule.NewScheduler(schedCfg, ng.DataService, ng.Cfg.AppURL, ng.stateManager)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -72,6 +73,8 @@ const (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
//TODO: temporary until fix org isolation
|
||||||
|
mainOrgID = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type Alertmanager struct {
|
type Alertmanager struct {
|
||||||
@@ -168,7 +171,7 @@ func (am *Alertmanager) Ready() bool {
|
|||||||
|
|
||||||
func (am *Alertmanager) Run(ctx context.Context) error {
|
func (am *Alertmanager) Run(ctx context.Context) error {
|
||||||
// Make sure dispatcher starts. We can tolerate future reload failures.
|
// Make sure dispatcher starts. We can tolerate future reload failures.
|
||||||
if err := am.SyncAndApplyConfigFromDatabase(); err != nil {
|
if err := am.SyncAndApplyConfigFromDatabase(mainOrgID); err != nil {
|
||||||
am.logger.Error("unable to sync configuration", "err", err)
|
am.logger.Error("unable to sync configuration", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +180,7 @@ func (am *Alertmanager) Run(ctx context.Context) error {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return am.StopAndWait()
|
return am.StopAndWait()
|
||||||
case <-time.After(pollInterval):
|
case <-time.After(pollInterval):
|
||||||
if err := am.SyncAndApplyConfigFromDatabase(); err != nil {
|
if err := am.SyncAndApplyConfigFromDatabase(mainOrgID); err != nil {
|
||||||
am.logger.Error("unable to sync configuration", "err", err)
|
am.logger.Error("unable to sync configuration", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +206,7 @@ func (am *Alertmanager) StopAndWait() error {
|
|||||||
|
|
||||||
// SaveAndApplyDefaultConfig saves the default configuration the database and applies the configuration to the Alertmanager.
|
// SaveAndApplyDefaultConfig saves the default configuration the database and applies the configuration to the Alertmanager.
|
||||||
// It rollbacks the save if we fail to apply the configuration.
|
// It rollbacks the save if we fail to apply the configuration.
|
||||||
func (am *Alertmanager) SaveAndApplyDefaultConfig() error {
|
func (am *Alertmanager) SaveAndApplyDefaultConfig(orgID int64) error {
|
||||||
am.reloadConfigMtx.Lock()
|
am.reloadConfigMtx.Lock()
|
||||||
defer am.reloadConfigMtx.Unlock()
|
defer am.reloadConfigMtx.Unlock()
|
||||||
|
|
||||||
@@ -211,6 +214,7 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig() error {
|
|||||||
AlertmanagerConfiguration: alertmanagerDefaultConfiguration,
|
AlertmanagerConfiguration: alertmanagerDefaultConfiguration,
|
||||||
Default: true,
|
Default: true,
|
||||||
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
|
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
|
||||||
|
OrgID: orgID,
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := Load([]byte(alertmanagerDefaultConfiguration))
|
cfg, err := Load([]byte(alertmanagerDefaultConfiguration))
|
||||||
@@ -234,7 +238,7 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig() error {
|
|||||||
|
|
||||||
// SaveAndApplyConfig saves the configuration the database and applies the configuration to the Alertmanager.
|
// SaveAndApplyConfig saves the configuration the database and applies the configuration to the Alertmanager.
|
||||||
// It rollbacks the save if we fail to apply the configuration.
|
// It rollbacks the save if we fail to apply the configuration.
|
||||||
func (am *Alertmanager) SaveAndApplyConfig(cfg *apimodels.PostableUserConfig) error {
|
func (am *Alertmanager) SaveAndApplyConfig(orgID int64, cfg *apimodels.PostableUserConfig) error {
|
||||||
rawConfig, err := json.Marshal(&cfg)
|
rawConfig, err := json.Marshal(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to serialize to the Alertmanager configuration: %w", err)
|
return fmt.Errorf("failed to serialize to the Alertmanager configuration: %w", err)
|
||||||
@@ -246,6 +250,7 @@ func (am *Alertmanager) SaveAndApplyConfig(cfg *apimodels.PostableUserConfig) er
|
|||||||
cmd := &ngmodels.SaveAlertmanagerConfigurationCmd{
|
cmd := &ngmodels.SaveAlertmanagerConfigurationCmd{
|
||||||
AlertmanagerConfiguration: string(rawConfig),
|
AlertmanagerConfiguration: string(rawConfig),
|
||||||
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
|
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
|
||||||
|
OrgID: orgID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.Store.SaveAlertmanagerConfigurationWithCallback(cmd, func() error {
|
err = am.Store.SaveAlertmanagerConfigurationWithCallback(cmd, func() error {
|
||||||
@@ -264,12 +269,12 @@ func (am *Alertmanager) SaveAndApplyConfig(cfg *apimodels.PostableUserConfig) er
|
|||||||
|
|
||||||
// SyncAndApplyConfigFromDatabase picks the latest config from database and restarts
|
// SyncAndApplyConfigFromDatabase picks the latest config from database and restarts
|
||||||
// the components with the new config.
|
// the components with the new config.
|
||||||
func (am *Alertmanager) SyncAndApplyConfigFromDatabase() error {
|
func (am *Alertmanager) SyncAndApplyConfigFromDatabase(orgID int64) error {
|
||||||
am.reloadConfigMtx.Lock()
|
am.reloadConfigMtx.Lock()
|
||||||
defer am.reloadConfigMtx.Unlock()
|
defer am.reloadConfigMtx.Unlock()
|
||||||
|
|
||||||
// First, let's get the configuration we need from the database.
|
// First, let's get the configuration we need from the database.
|
||||||
q := &ngmodels.GetLatestAlertmanagerConfigurationQuery{}
|
q := &ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: mainOrgID}
|
||||||
if err := am.Store.GetLatestAlertmanagerConfiguration(q); err != nil {
|
if err := am.Store.GetLatestAlertmanagerConfiguration(q); err != nil {
|
||||||
// If there's no configuration in the database, let's use the default configuration.
|
// If there's no configuration in the database, let's use the default configuration.
|
||||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||||
@@ -279,6 +284,7 @@ func (am *Alertmanager) SyncAndApplyConfigFromDatabase() error {
|
|||||||
AlertmanagerConfiguration: alertmanagerDefaultConfiguration,
|
AlertmanagerConfiguration: alertmanagerDefaultConfiguration,
|
||||||
Default: true,
|
Default: true,
|
||||||
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
|
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
|
||||||
|
OrgID: orgID,
|
||||||
}
|
}
|
||||||
if err := am.Store.SaveAlertmanagerConfiguration(savecmd); err != nil {
|
if err := am.Store.SaveAlertmanagerConfiguration(savecmd); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -399,7 +405,7 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (am *Alertmanager) WorkingDirPath() string {
|
func (am *Alertmanager) WorkingDirPath() string {
|
||||||
return filepath.Join(am.Settings.DataPath, workingDir)
|
return filepath.Join(am.Settings.DataPath, workingDir, strconv.Itoa(mainOrgID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildIntegrationsMap builds a map of name to the list of Grafana integration notifiers off of a list of receiver config.
|
// buildIntegrationsMap builds a map of name to the list of Grafana integration notifiers off of a list of receiver config.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func setupAMTest(t *testing.T) *Alertmanager {
|
|||||||
|
|
||||||
func TestAlertmanager_ShouldUseDefaultConfigurationWhenNoConfiguration(t *testing.T) {
|
func TestAlertmanager_ShouldUseDefaultConfigurationWhenNoConfiguration(t *testing.T) {
|
||||||
am := setupAMTest(t)
|
am := setupAMTest(t)
|
||||||
require.NoError(t, am.SyncAndApplyConfigFromDatabase())
|
require.NoError(t, am.SyncAndApplyConfigFromDatabase(mainOrgID))
|
||||||
require.NotNil(t, am.config)
|
require.NotNil(t, am.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ import (
|
|||||||
// timeNow makes it possible to test usage of time
|
// timeNow makes it possible to test usage of time
|
||||||
var timeNow = time.Now
|
var timeNow = time.Now
|
||||||
|
|
||||||
// AdminConfigPollingInterval of how often we sync admin configuration.
|
|
||||||
var AdminConfigPollingInterval = 1 * time.Minute
|
|
||||||
|
|
||||||
// ScheduleService handles scheduling
|
// ScheduleService handles scheduling
|
||||||
type ScheduleService interface {
|
type ScheduleService interface {
|
||||||
Run(context.Context) error
|
Run(context.Context) error
|
||||||
Pause() error
|
Pause() error
|
||||||
Unpause() error
|
Unpause() error
|
||||||
|
AlertmanagersFor(orgID int64) []*url.URL
|
||||||
|
DroppedAlertmanagersFor(orgID int64) []*url.URL
|
||||||
|
|
||||||
// the following are used by tests only used for tests
|
// the following are used by tests only used for tests
|
||||||
evalApplied(models.AlertRuleKey, time.Time)
|
evalApplied(models.AlertRuleKey, time.Time)
|
||||||
@@ -85,50 +84,53 @@ type schedule struct {
|
|||||||
metrics *metrics.Metrics
|
metrics *metrics.Metrics
|
||||||
|
|
||||||
// Senders help us send alerts to external Alertmanagers.
|
// Senders help us send alerts to external Alertmanagers.
|
||||||
sendersMtx sync.RWMutex
|
sendersMtx sync.RWMutex
|
||||||
sendersCfgHash map[int64]string
|
sendersCfgHash map[int64]string
|
||||||
senders map[int64]*sender.Sender
|
senders map[int64]*sender.Sender
|
||||||
|
adminConfigPollInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchedulerCfg is the scheduler configuration.
|
// SchedulerCfg is the scheduler configuration.
|
||||||
type SchedulerCfg struct {
|
type SchedulerCfg struct {
|
||||||
C clock.Clock
|
C clock.Clock
|
||||||
BaseInterval time.Duration
|
BaseInterval time.Duration
|
||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
EvalAppliedFunc func(models.AlertRuleKey, time.Time)
|
EvalAppliedFunc func(models.AlertRuleKey, time.Time)
|
||||||
MaxAttempts int64
|
MaxAttempts int64
|
||||||
StopAppliedFunc func(models.AlertRuleKey)
|
StopAppliedFunc func(models.AlertRuleKey)
|
||||||
Evaluator eval.Evaluator
|
Evaluator eval.Evaluator
|
||||||
RuleStore store.RuleStore
|
RuleStore store.RuleStore
|
||||||
InstanceStore store.InstanceStore
|
InstanceStore store.InstanceStore
|
||||||
AdminConfigStore store.AdminConfigurationStore
|
AdminConfigStore store.AdminConfigurationStore
|
||||||
Notifier Notifier
|
Notifier Notifier
|
||||||
Metrics *metrics.Metrics
|
Metrics *metrics.Metrics
|
||||||
|
AdminConfigPollInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScheduler returns a new schedule.
|
// NewScheduler returns a new schedule.
|
||||||
func NewScheduler(cfg SchedulerCfg, dataService *tsdb.Service, appURL string, stateManager *state.Manager) *schedule {
|
func NewScheduler(cfg SchedulerCfg, dataService *tsdb.Service, appURL string, stateManager *state.Manager) *schedule {
|
||||||
ticker := alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
|
ticker := alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
|
||||||
sch := schedule{
|
sch := schedule{
|
||||||
registry: alertRuleRegistry{alertRuleInfo: make(map[models.AlertRuleKey]alertRuleInfo)},
|
registry: alertRuleRegistry{alertRuleInfo: make(map[models.AlertRuleKey]alertRuleInfo)},
|
||||||
maxAttempts: cfg.MaxAttempts,
|
maxAttempts: cfg.MaxAttempts,
|
||||||
clock: cfg.C,
|
clock: cfg.C,
|
||||||
baseInterval: cfg.BaseInterval,
|
baseInterval: cfg.BaseInterval,
|
||||||
log: cfg.Logger,
|
log: cfg.Logger,
|
||||||
heartbeat: ticker,
|
heartbeat: ticker,
|
||||||
evalAppliedFunc: cfg.EvalAppliedFunc,
|
evalAppliedFunc: cfg.EvalAppliedFunc,
|
||||||
stopAppliedFunc: cfg.StopAppliedFunc,
|
stopAppliedFunc: cfg.StopAppliedFunc,
|
||||||
evaluator: cfg.Evaluator,
|
evaluator: cfg.Evaluator,
|
||||||
ruleStore: cfg.RuleStore,
|
ruleStore: cfg.RuleStore,
|
||||||
instanceStore: cfg.InstanceStore,
|
instanceStore: cfg.InstanceStore,
|
||||||
dataService: dataService,
|
dataService: dataService,
|
||||||
adminConfigStore: cfg.AdminConfigStore,
|
adminConfigStore: cfg.AdminConfigStore,
|
||||||
notifier: cfg.Notifier,
|
notifier: cfg.Notifier,
|
||||||
metrics: cfg.Metrics,
|
metrics: cfg.Metrics,
|
||||||
appURL: appURL,
|
appURL: appURL,
|
||||||
stateManager: stateManager,
|
stateManager: stateManager,
|
||||||
senders: map[int64]*sender.Sender{},
|
senders: map[int64]*sender.Sender{},
|
||||||
sendersCfgHash: map[int64]string{},
|
sendersCfgHash: map[int64]string{},
|
||||||
|
adminConfigPollInterval: cfg.AdminConfigPollInterval,
|
||||||
}
|
}
|
||||||
return &sch
|
return &sch
|
||||||
}
|
}
|
||||||
@@ -290,7 +292,7 @@ func (sch *schedule) DroppedAlertmanagersFor(orgID int64) []*url.URL {
|
|||||||
func (sch *schedule) adminConfigSync(ctx context.Context) error {
|
func (sch *schedule) adminConfigSync(ctx context.Context) error {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(AdminConfigPollingInterval):
|
case <-time.After(sch.adminConfigPollInterval):
|
||||||
if err := sch.SyncAndApplyConfigFromDatabase(); err != nil {
|
if err := sch.SyncAndApplyConfigFromDatabase(); err != nil {
|
||||||
sch.log.Error("unable to sync admin configuration", "err", err)
|
sch.log.Error("unable to sync admin configuration", "err", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,9 +99,10 @@ func TestWarmStateCache(t *testing.T) {
|
|||||||
BaseInterval: time.Second,
|
BaseInterval: time.Second,
|
||||||
Logger: log.New("ngalert cache warming test"),
|
Logger: log.New("ngalert cache warming test"),
|
||||||
|
|
||||||
RuleStore: dbstore,
|
RuleStore: dbstore,
|
||||||
InstanceStore: dbstore,
|
InstanceStore: dbstore,
|
||||||
Metrics: metrics.NewMetrics(prometheus.NewRegistry()),
|
Metrics: metrics.NewMetrics(prometheus.NewRegistry()),
|
||||||
|
AdminConfigPollInterval: 10 * time.Minute, // do not poll in unit tests.
|
||||||
}
|
}
|
||||||
st := state.NewManager(schedCfg.Logger, nilMetrics, dbstore, dbstore)
|
st := state.NewManager(schedCfg.Logger, nilMetrics, dbstore, dbstore)
|
||||||
st.Warm()
|
st.Warm()
|
||||||
@@ -143,10 +144,11 @@ func TestAlertingTicker(t *testing.T) {
|
|||||||
StopAppliedFunc: func(alertDefKey models.AlertRuleKey) {
|
StopAppliedFunc: func(alertDefKey models.AlertRuleKey) {
|
||||||
stopAppliedCh <- alertDefKey
|
stopAppliedCh <- alertDefKey
|
||||||
},
|
},
|
||||||
RuleStore: dbstore,
|
RuleStore: dbstore,
|
||||||
InstanceStore: dbstore,
|
InstanceStore: dbstore,
|
||||||
Logger: log.New("ngalert schedule test"),
|
Logger: log.New("ngalert schedule test"),
|
||||||
Metrics: metrics.NewMetrics(prometheus.NewRegistry()),
|
Metrics: metrics.NewMetrics(prometheus.NewRegistry()),
|
||||||
|
AdminConfigPollInterval: 10 * time.Minute, // do not poll in unit tests.
|
||||||
}
|
}
|
||||||
st := state.NewManager(schedCfg.Logger, nilMetrics, dbstore, dbstore)
|
st := state.NewManager(schedCfg.Logger, nilMetrics, dbstore, dbstore)
|
||||||
sched := schedule.NewScheduler(schedCfg, nil, "http://localhost", st)
|
sched := schedule.NewScheduler(schedCfg, nil, "http://localhost", st)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
func TestSendingToExternalAlertmanager(t *testing.T) {
|
func TestSendingToExternalAlertmanager(t *testing.T) {
|
||||||
t.Cleanup(registry.ClearOverrides)
|
t.Cleanup(registry.ClearOverrides)
|
||||||
|
|
||||||
fakeAM := newFakeExternalAlertmanager(t)
|
fakeAM := NewFakeExternalAlertmanager(t)
|
||||||
defer fakeAM.Close()
|
defer fakeAM.Close()
|
||||||
fakeRuleStore := newFakeRuleStore(t)
|
fakeRuleStore := newFakeRuleStore(t)
|
||||||
fakeInstanceStore := &fakeInstanceStore{}
|
fakeInstanceStore := &fakeInstanceStore{}
|
||||||
@@ -61,7 +61,6 @@ func TestSendingToExternalAlertmanager(t *testing.T) {
|
|||||||
cancel()
|
cancel()
|
||||||
})
|
})
|
||||||
go func() {
|
go func() {
|
||||||
AdminConfigPollingInterval = 10 * time.Minute // Do not poll in unit tests.
|
|
||||||
err := sched.Run(ctx)
|
err := sched.Run(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
@@ -95,7 +94,7 @@ func TestSendingToExternalAlertmanager(t *testing.T) {
|
|||||||
func TestSendingToExternalAlertmanager_WithMultipleOrgs(t *testing.T) {
|
func TestSendingToExternalAlertmanager_WithMultipleOrgs(t *testing.T) {
|
||||||
t.Cleanup(registry.ClearOverrides)
|
t.Cleanup(registry.ClearOverrides)
|
||||||
|
|
||||||
fakeAM := newFakeExternalAlertmanager(t)
|
fakeAM := NewFakeExternalAlertmanager(t)
|
||||||
defer fakeAM.Close()
|
defer fakeAM.Close()
|
||||||
fakeRuleStore := newFakeRuleStore(t)
|
fakeRuleStore := newFakeRuleStore(t)
|
||||||
fakeInstanceStore := &fakeInstanceStore{}
|
fakeInstanceStore := &fakeInstanceStore{}
|
||||||
@@ -130,7 +129,6 @@ func TestSendingToExternalAlertmanager_WithMultipleOrgs(t *testing.T) {
|
|||||||
cancel()
|
cancel()
|
||||||
})
|
})
|
||||||
go func() {
|
go func() {
|
||||||
AdminConfigPollingInterval = 10 * time.Minute // Do not poll in unit tests.
|
|
||||||
err := sched.Run(ctx)
|
err := sched.Run(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
@@ -161,7 +159,7 @@ func TestSendingToExternalAlertmanager_WithMultipleOrgs(t *testing.T) {
|
|||||||
}, 20*time.Second, 200*time.Millisecond)
|
}, 20*time.Second, 200*time.Millisecond)
|
||||||
|
|
||||||
// 2. Next, let's modify the configuration of an organization by adding an extra alertmanager.
|
// 2. Next, let's modify the configuration of an organization by adding an extra alertmanager.
|
||||||
fakeAM2 := newFakeExternalAlertmanager(t)
|
fakeAM2 := NewFakeExternalAlertmanager(t)
|
||||||
adminConfig2 = &models.AdminConfiguration{OrgID: 2, Alertmanagers: []string{fakeAM.server.URL, fakeAM2.server.URL}}
|
adminConfig2 = &models.AdminConfiguration{OrgID: 2, Alertmanagers: []string{fakeAM.server.URL, fakeAM2.server.URL}}
|
||||||
cmd = store.UpdateAdminConfigurationCmd{AdminConfiguration: adminConfig2}
|
cmd = store.UpdateAdminConfigurationCmd{AdminConfiguration: adminConfig2}
|
||||||
require.NoError(t, fakeAdminConfigStore.UpdateAdminConfiguration(cmd))
|
require.NoError(t, fakeAdminConfigStore.UpdateAdminConfiguration(cmd))
|
||||||
@@ -238,16 +236,17 @@ func setupScheduler(t *testing.T, rs store.RuleStore, is store.InstanceStore, ac
|
|||||||
logger := log.New("ngalert schedule test")
|
logger := log.New("ngalert schedule test")
|
||||||
nilMetrics := metrics.NewMetrics(nil)
|
nilMetrics := metrics.NewMetrics(nil)
|
||||||
schedCfg := SchedulerCfg{
|
schedCfg := SchedulerCfg{
|
||||||
C: mockedClock,
|
C: mockedClock,
|
||||||
BaseInterval: time.Second,
|
BaseInterval: time.Second,
|
||||||
MaxAttempts: 1,
|
MaxAttempts: 1,
|
||||||
Evaluator: eval.Evaluator{Cfg: &setting.Cfg{ExpressionsEnabled: true}, Log: logger},
|
Evaluator: eval.Evaluator{Cfg: &setting.Cfg{ExpressionsEnabled: true}, Log: logger},
|
||||||
RuleStore: rs,
|
RuleStore: rs,
|
||||||
InstanceStore: is,
|
InstanceStore: is,
|
||||||
AdminConfigStore: acs,
|
AdminConfigStore: acs,
|
||||||
Notifier: &fakeNotifier{},
|
Notifier: &fakeNotifier{},
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Metrics: metrics.NewMetrics(prometheus.NewRegistry()),
|
Metrics: metrics.NewMetrics(prometheus.NewRegistry()),
|
||||||
|
AdminConfigPollInterval: 10 * time.Minute, // do not poll in unit tests.
|
||||||
}
|
}
|
||||||
st := state.NewManager(schedCfg.Logger, nilMetrics, rs, is)
|
st := state.NewManager(schedCfg.Logger, nilMetrics, rs, is)
|
||||||
return NewScheduler(schedCfg, nil, "http://localhost", st), mockedClock
|
return NewScheduler(schedCfg, nil, "http://localhost", st), mockedClock
|
||||||
|
|||||||
@@ -237,17 +237,17 @@ func (n *fakeNotifier) PutAlerts(alerts apimodels.PostableAlerts) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeExternalAlertmanager struct {
|
type FakeExternalAlertmanager struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
mtx sync.Mutex
|
mtx sync.Mutex
|
||||||
alerts amv2.PostableAlerts
|
alerts amv2.PostableAlerts
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFakeExternalAlertmanager(t *testing.T) *fakeExternalAlertmanager {
|
func NewFakeExternalAlertmanager(t *testing.T) *FakeExternalAlertmanager {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
am := &fakeExternalAlertmanager{
|
am := &FakeExternalAlertmanager{
|
||||||
t: t,
|
t: t,
|
||||||
alerts: amv2.PostableAlerts{},
|
alerts: amv2.PostableAlerts{},
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,11 @@ func newFakeExternalAlertmanager(t *testing.T) *fakeExternalAlertmanager {
|
|||||||
return am
|
return am
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *fakeExternalAlertmanager) AlertNamesCompare(expected []string) bool {
|
func (am *FakeExternalAlertmanager) URL() string {
|
||||||
|
return am.server.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *FakeExternalAlertmanager) AlertNamesCompare(expected []string) bool {
|
||||||
n := []string{}
|
n := []string{}
|
||||||
alerts := am.Alerts()
|
alerts := am.Alerts()
|
||||||
|
|
||||||
@@ -275,20 +279,20 @@ func (am *fakeExternalAlertmanager) AlertNamesCompare(expected []string) bool {
|
|||||||
return assert.ObjectsAreEqual(expected, n)
|
return assert.ObjectsAreEqual(expected, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *fakeExternalAlertmanager) AlertsCount() int {
|
func (am *FakeExternalAlertmanager) AlertsCount() int {
|
||||||
am.mtx.Lock()
|
am.mtx.Lock()
|
||||||
defer am.mtx.Unlock()
|
defer am.mtx.Unlock()
|
||||||
|
|
||||||
return len(am.alerts)
|
return len(am.alerts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *fakeExternalAlertmanager) Alerts() amv2.PostableAlerts {
|
func (am *FakeExternalAlertmanager) Alerts() amv2.PostableAlerts {
|
||||||
am.mtx.Lock()
|
am.mtx.Lock()
|
||||||
defer am.mtx.Unlock()
|
defer am.mtx.Unlock()
|
||||||
return am.alerts
|
return am.alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *fakeExternalAlertmanager) Handler() func(w http.ResponseWriter, r *http.Request) {
|
func (am *FakeExternalAlertmanager) Handler() func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
b, err := ioutil.ReadAll(r.Body)
|
b, err := ioutil.ReadAll(r.Body)
|
||||||
require.NoError(am.t, err)
|
require.NoError(am.t, err)
|
||||||
@@ -302,6 +306,6 @@ func (am *fakeExternalAlertmanager) Handler() func(w http.ResponseWriter, r *htt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *fakeExternalAlertmanager) Close() {
|
func (am *FakeExternalAlertmanager) Close() {
|
||||||
am.server.Close()
|
am.server.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ func (st *Manager) setNextState(alertRule *ngModels.AlertRule, result eval.Resul
|
|||||||
|
|
||||||
st.set(currentState)
|
st.set(currentState)
|
||||||
if oldState != currentState.State {
|
if oldState != currentState.State {
|
||||||
go st.createAlertAnnotation(currentState.State, alertRule, result)
|
go st.createAlertAnnotation(currentState.State, alertRule, result, oldState)
|
||||||
}
|
}
|
||||||
return currentState
|
return currentState
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *Manager) createAlertAnnotation(new eval.State, alertRule *ngModels.AlertRule, result eval.Result) {
|
func (st *Manager) createAlertAnnotation(new eval.State, alertRule *ngModels.AlertRule, result eval.Result, oldState eval.State) {
|
||||||
st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", new.String())
|
st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", new.String())
|
||||||
dashUid, ok := alertRule.Annotations["__dashboardUid__"]
|
dashUid, ok := alertRule.Annotations["__dashboardUid__"]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -255,12 +255,14 @@ func (st *Manager) createAlertAnnotation(new eval.State, alertRule *ngModels.Ale
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationText := fmt.Sprintf("%s %s", result.Instance.String(), new.String())
|
annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, result.Instance.String(), new.String())
|
||||||
|
|
||||||
item := &annotations.Item{
|
item := &annotations.Item{
|
||||||
OrgId: alertRule.OrgID,
|
OrgId: alertRule.OrgID,
|
||||||
DashboardId: query.Result.Id,
|
DashboardId: query.Result.Id,
|
||||||
PanelId: panelId,
|
PanelId: panelId,
|
||||||
|
PrevState: oldState.String(),
|
||||||
|
NewState: new.String(),
|
||||||
Text: annotationText,
|
Text: annotationText,
|
||||||
Epoch: result.EvaluatedAt.UnixNano() / int64(time.Millisecond),
|
Epoch: result.EvaluatedAt.UnixNano() / int64(time.Millisecond),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (st *DBstore) GetLatestAlertmanagerConfiguration(query *models.GetLatestAle
|
|||||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
c := &models.AlertConfiguration{}
|
c := &models.AlertConfiguration{}
|
||||||
// The ID is already an auto incremental column, using the ID as an order should guarantee the latest.
|
// The ID is already an auto incremental column, using the ID as an order should guarantee the latest.
|
||||||
ok, err := sess.Desc("id").Limit(1).Get(c)
|
ok, err := sess.Desc("id").Where("org_id = ?", query.OrgID).Limit(1).Get(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,7 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(cmd *models.SaveAler
|
|||||||
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
||||||
ConfigurationVersion: cmd.ConfigurationVersion,
|
ConfigurationVersion: cmd.ConfigurationVersion,
|
||||||
Default: cmd.Default,
|
Default: cmd.Default,
|
||||||
|
OrgID: cmd.OrgID,
|
||||||
}
|
}
|
||||||
if _, err := sess.Insert(config); err != nil {
|
if _, err := sess.Insert(config); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func AddMigrations(mg *Migrator) {
|
|||||||
ualert.AddTablesMigrations(mg)
|
ualert.AddTablesMigrations(mg)
|
||||||
ualert.AddDashAlertMigration(mg)
|
ualert.AddDashAlertMigration(mg)
|
||||||
addLibraryElementsMigrations(mg)
|
addLibraryElementsMigrations(mg)
|
||||||
|
ualert.RerunDashAlertMigration(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type alertRule struct {
|
type alertRule struct {
|
||||||
OrgId int64
|
OrgID int64 `xorm:"org_id"`
|
||||||
Title string
|
Title string
|
||||||
Condition string
|
Condition string
|
||||||
Data []alertQuery
|
Data []alertQuery
|
||||||
IntervalSeconds int64
|
IntervalSeconds int64
|
||||||
Version int64
|
Version int64
|
||||||
Uid string
|
UID string `xorm:"uid"`
|
||||||
NamespaceUid string
|
NamespaceUID string `xorm:"namespace_uid"`
|
||||||
RuleGroup string
|
RuleGroup string
|
||||||
NoDataState string
|
NoDataState string
|
||||||
ExecErrState string
|
ExecErrState string
|
||||||
@@ -51,9 +51,9 @@ type alertRuleVersion struct {
|
|||||||
|
|
||||||
func (a *alertRule) makeVersion() *alertRuleVersion {
|
func (a *alertRule) makeVersion() *alertRuleVersion {
|
||||||
return &alertRuleVersion{
|
return &alertRuleVersion{
|
||||||
RuleOrgID: a.OrgId,
|
RuleOrgID: a.OrgID,
|
||||||
RuleUID: a.Uid,
|
RuleUID: a.UID,
|
||||||
RuleNamespaceUID: a.NamespaceUid,
|
RuleNamespaceUID: a.NamespaceUID,
|
||||||
RuleGroup: a.RuleGroup,
|
RuleGroup: a.RuleGroup,
|
||||||
ParentVersion: 0,
|
ParentVersion: 0,
|
||||||
RestoredFrom: 0,
|
RestoredFrom: 0,
|
||||||
@@ -96,14 +96,14 @@ func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string
|
|||||||
annotations["message"] = da.Message
|
annotations["message"] = da.Message
|
||||||
|
|
||||||
ar := &alertRule{
|
ar := &alertRule{
|
||||||
OrgId: da.OrgId,
|
OrgID: da.OrgId,
|
||||||
Title: da.Name, // TODO: Make sure all names are unique, make new name on constraint insert error.
|
Title: da.Name, // TODO: Make sure all names are unique, make new name on constraint insert error.
|
||||||
Uid: util.GenerateShortUID(),
|
UID: util.GenerateShortUID(),
|
||||||
Condition: cond.Condition,
|
Condition: cond.Condition,
|
||||||
Data: cond.Data,
|
Data: cond.Data,
|
||||||
IntervalSeconds: ruleAdjustInterval(da.Frequency),
|
IntervalSeconds: ruleAdjustInterval(da.Frequency),
|
||||||
Version: 1,
|
Version: 1,
|
||||||
NamespaceUid: folderUID, // Folder already created, comes from env var.
|
NamespaceUID: folderUID, // Folder already created, comes from env var.
|
||||||
RuleGroup: da.Name,
|
RuleGroup: da.Name,
|
||||||
For: duration(da.For),
|
For: duration(da.For),
|
||||||
Updated: time.Now().UTC(),
|
Updated: time.Now().UTC(),
|
||||||
@@ -123,7 +123,7 @@ func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Label for routing and silences.
|
// Label for routing and silences.
|
||||||
n, v := getLabelForRouteMatching(ar.Uid)
|
n, v := getLabelForRouteMatching(ar.UID)
|
||||||
ar.Labels[n] = v
|
ar.Labels[n] = v
|
||||||
|
|
||||||
if err := m.addSilence(da, ar); err != nil {
|
if err := m.addSilence(da, ar); err != nil {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type notificationChannel struct {
|
type notificationChannel struct {
|
||||||
ID int `xorm:"id"`
|
ID int64 `xorm:"id"`
|
||||||
|
OrgID int64 `xorm:"org_id"`
|
||||||
Uid string `xorm:"uid"`
|
Uid string `xorm:"uid"`
|
||||||
Name string `xorm:"name"`
|
Name string `xorm:"name"`
|
||||||
Type string `xorm:"type"`
|
Type string `xorm:"type"`
|
||||||
@@ -27,9 +28,16 @@ type notificationChannel struct {
|
|||||||
SecureSettings securejsondata.SecureJsonData `xorm:"secure_settings"`
|
SecureSettings securejsondata.SecureJsonData `xorm:"secure_settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) getNotificationChannelMap() (map[interface{}]*notificationChannel, []*notificationChannel, error) {
|
// channelsPerOrg maps notification channels per organisation
|
||||||
|
type channelsPerOrg map[int64]map[interface{}]*notificationChannel
|
||||||
|
|
||||||
|
// channelMap maps notification channels per organisation
|
||||||
|
type defaultChannelsPerOrg map[int64][]*notificationChannel
|
||||||
|
|
||||||
|
func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannelsPerOrg, error) {
|
||||||
q := `
|
q := `
|
||||||
SELECT id,
|
SELECT id,
|
||||||
|
org_id,
|
||||||
uid,
|
uid,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
@@ -50,25 +58,27 @@ func (m *migration) getNotificationChannelMap() (map[interface{}]*notificationCh
|
|||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allChannelsMap := make(map[interface{}]*notificationChannel)
|
allChannelsMap := make(channelsPerOrg)
|
||||||
var defaultChannels []*notificationChannel
|
defaultChannelsMap := make(defaultChannelsPerOrg)
|
||||||
for i, c := range allChannels {
|
for i, c := range allChannels {
|
||||||
|
if _, ok := allChannelsMap[c.OrgID]; !ok { // new seen org
|
||||||
|
allChannelsMap[c.OrgID] = make(map[interface{}]*notificationChannel)
|
||||||
|
}
|
||||||
if c.Uid != "" {
|
if c.Uid != "" {
|
||||||
allChannelsMap[c.Uid] = &allChannels[i]
|
allChannelsMap[c.OrgID][c.Uid] = &allChannels[i]
|
||||||
}
|
}
|
||||||
if c.ID != 0 {
|
if c.ID != 0 {
|
||||||
allChannelsMap[c.ID] = &allChannels[i]
|
allChannelsMap[c.OrgID][c.ID] = &allChannels[i]
|
||||||
}
|
}
|
||||||
if c.IsDefault {
|
if c.IsDefault {
|
||||||
// TODO: verify that there will be only 1 default channel.
|
defaultChannelsMap[c.OrgID] = append(defaultChannelsMap[c.OrgID], &allChannels[i])
|
||||||
defaultChannels = append(defaultChannels, &allChannels[i])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allChannelsMap, defaultChannels, nil
|
return allChannelsMap, defaultChannelsMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) updateReceiverAndRoute(allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel, da dashAlert, rule *alertRule, amConfig *PostableUserConfig) error {
|
func (m *migration) updateReceiverAndRoute(allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg, da dashAlert, rule *alertRule, amConfig *PostableUserConfig) error {
|
||||||
// Create receiver and route for this rule.
|
// Create receiver and route for this rule.
|
||||||
if allChannels == nil {
|
if allChannels == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -82,7 +92,7 @@ func (m *migration) updateReceiverAndRoute(allChannels map[interface{}]*notifica
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
recv, route, err := m.makeReceiverAndRoute(rule.Uid, channelIDs, defaultChannels, allChannels)
|
recv, route, err := m.makeReceiverAndRoute(rule.UID, rule.OrgID, channelIDs, defaultChannels[rule.OrgID], allChannels[rule.OrgID])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -97,7 +107,7 @@ func (m *migration) updateReceiverAndRoute(allChannels map[interface{}]*notifica
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface{}, defaultChannels []*notificationChannel, allChannels map[interface{}]*notificationChannel) (*PostableApiReceiver, *Route, error) {
|
func (m *migration) makeReceiverAndRoute(ruleUid string, orgID int64, channelUids []interface{}, defaultChannels []*notificationChannel, allChannels map[interface{}]*notificationChannel) (*PostableApiReceiver, *Route, error) {
|
||||||
portedChannels := []*PostableGrafanaReceiver{}
|
portedChannels := []*PostableGrafanaReceiver{}
|
||||||
var receiver *PostableApiReceiver
|
var receiver *PostableApiReceiver
|
||||||
|
|
||||||
@@ -112,7 +122,10 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
|
|||||||
return errors.New("failed to generate UID for notification channel")
|
return errors.New("failed to generate UID for notification channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.migratedChannels[c] = struct{}{}
|
if _, ok := m.migratedChannelsPerOrg[orgID]; !ok {
|
||||||
|
m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{})
|
||||||
|
}
|
||||||
|
m.migratedChannelsPerOrg[orgID][c] = struct{}{}
|
||||||
settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
|
settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
|
||||||
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
|
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
|
||||||
UID: uid,
|
UID: uid,
|
||||||
@@ -129,9 +142,10 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
|
|||||||
// Remove obsolete notification channels.
|
// Remove obsolete notification channels.
|
||||||
filteredChannelUids := make(map[interface{}]struct{})
|
filteredChannelUids := make(map[interface{}]struct{})
|
||||||
for _, uid := range channelUids {
|
for _, uid := range channelUids {
|
||||||
_, ok := allChannels[uid]
|
c, ok := allChannels[uid]
|
||||||
if ok {
|
if ok {
|
||||||
filteredChannelUids[uid] = struct{}{}
|
// always store the channel UID to prevent duplicates
|
||||||
|
filteredChannelUids[c.Uid] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
m.mg.Logger.Warn("ignoring obsolete notification channel", "uid", uid)
|
m.mg.Logger.Warn("ignoring obsolete notification channel", "uid", uid)
|
||||||
}
|
}
|
||||||
@@ -142,9 +156,10 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
|
|||||||
if c.Uid == "" {
|
if c.Uid == "" {
|
||||||
id = c.ID
|
id = c.ID
|
||||||
}
|
}
|
||||||
_, ok := allChannels[id]
|
c, ok := allChannels[id]
|
||||||
if ok {
|
if ok {
|
||||||
filteredChannelUids[id] = struct{}{}
|
// always store the channel UID to prevent duplicates
|
||||||
|
filteredChannelUids[c.Uid] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +174,11 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
|
|||||||
}
|
}
|
||||||
|
|
||||||
var receiverName string
|
var receiverName string
|
||||||
if rn, ok := m.portedChannelGroups[chanKey]; ok {
|
|
||||||
|
if _, ok := m.portedChannelGroupsPerOrg[orgID]; !ok {
|
||||||
|
m.portedChannelGroupsPerOrg[orgID] = make(map[string]string)
|
||||||
|
}
|
||||||
|
if rn, ok := m.portedChannelGroupsPerOrg[orgID][chanKey]; ok {
|
||||||
// We have ported these exact set of channels already. Re-use it.
|
// We have ported these exact set of channels already. Re-use it.
|
||||||
receiverName = rn
|
receiverName = rn
|
||||||
if receiverName == "autogen-contact-point-default" {
|
if receiverName == "autogen-contact-point-default" {
|
||||||
@@ -180,7 +199,7 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
|
|||||||
receiverName = fmt.Sprintf("autogen-contact-point-%d", m.lastReceiverID)
|
receiverName = fmt.Sprintf("autogen-contact-point-%d", m.lastReceiverID)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.portedChannelGroups[chanKey] = receiverName
|
m.portedChannelGroupsPerOrg[orgID][chanKey] = receiverName
|
||||||
receiver = &PostableApiReceiver{
|
receiver = &PostableApiReceiver{
|
||||||
Name: receiverName,
|
Name: receiverName,
|
||||||
GrafanaManagedReceivers: portedChannels,
|
GrafanaManagedReceivers: portedChannels,
|
||||||
@@ -220,32 +239,47 @@ func makeKeyForChannelGroup(channelUids map[interface{}]struct{}) (string, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addDefaultChannels should be called before adding any other routes.
|
// addDefaultChannels should be called before adding any other routes.
|
||||||
func (m *migration) addDefaultChannels(amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error {
|
func (m *migration) addDefaultChannels(amConfigsPerOrg amConfigsPerOrg, allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg) error {
|
||||||
// Default route and receiver.
|
for orgID := range allChannels {
|
||||||
recv, route, err := m.makeReceiverAndRoute("default_route", nil, defaultChannels, allChannels)
|
if _, ok := amConfigsPerOrg[orgID]; !ok {
|
||||||
if err != nil {
|
amConfigsPerOrg[orgID] = &PostableUserConfig{
|
||||||
return err
|
AlertmanagerConfig: PostableApiAlertingConfig{
|
||||||
}
|
Receivers: make([]*PostableApiReceiver, 0),
|
||||||
|
Route: &Route{
|
||||||
|
Routes: make([]*Route, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default route and receiver.
|
||||||
|
recv, route, err := m.makeReceiverAndRoute("default_route", orgID, nil, defaultChannels[orgID], allChannels[orgID])
|
||||||
|
if err != nil {
|
||||||
|
// if one fails it will fail the migration
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if recv != nil {
|
if recv != nil {
|
||||||
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, recv)
|
amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers = append(amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers, recv)
|
||||||
|
}
|
||||||
|
if route != nil {
|
||||||
|
route.Matchers = nil // Don't need matchers for root route.
|
||||||
|
amConfigsPerOrg[orgID].AlertmanagerConfig.Route = route
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if route != nil {
|
|
||||||
route.Matchers = nil // Don't need matchers for root route.
|
|
||||||
amConfig.AlertmanagerConfig.Route = route
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) addUnmigratedChannels(amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error {
|
func (m *migration) addUnmigratedChannels(orgID int64, amConfigs *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error {
|
||||||
// Unmigrated channels.
|
// Unmigrated channels.
|
||||||
portedChannels := []*PostableGrafanaReceiver{}
|
portedChannels := []*PostableGrafanaReceiver{}
|
||||||
receiver := &PostableApiReceiver{
|
receiver := &PostableApiReceiver{
|
||||||
Name: "autogen-unlinked-channel-recv",
|
Name: "autogen-unlinked-channel-recv",
|
||||||
}
|
}
|
||||||
for _, c := range allChannels {
|
for _, c := range allChannels {
|
||||||
_, ok := m.migratedChannels[c]
|
if _, ok := m.migratedChannelsPerOrg[orgID]; !ok {
|
||||||
|
m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{})
|
||||||
|
}
|
||||||
|
_, ok := m.migratedChannelsPerOrg[orgID][c]
|
||||||
if ok {
|
if ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -259,7 +293,7 @@ func (m *migration) addUnmigratedChannels(amConfig *PostableUserConfig, allChann
|
|||||||
return errors.New("failed to generate UID for notification channel")
|
return errors.New("failed to generate UID for notification channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.migratedChannels[c] = struct{}{}
|
m.migratedChannelsPerOrg[orgID][c] = struct{}{}
|
||||||
settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
|
settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
|
||||||
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
|
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
|
||||||
UID: uid,
|
UID: uid,
|
||||||
@@ -272,7 +306,7 @@ func (m *migration) addUnmigratedChannels(amConfig *PostableUserConfig, allChann
|
|||||||
}
|
}
|
||||||
receiver.GrafanaManagedReceivers = portedChannels
|
receiver.GrafanaManagedReceivers = portedChannels
|
||||||
if len(portedChannels) > 0 {
|
if len(portedChannels) > 0 {
|
||||||
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, receiver)
|
amConfigs.AlertmanagerConfig.Receivers = append(amConfigs.AlertmanagerConfig.Receivers, receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -361,6 +395,8 @@ type PostableUserConfig struct {
|
|||||||
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type amConfigsPerOrg = map[int64]*PostableUserConfig
|
||||||
|
|
||||||
func (c *PostableUserConfig) EncryptSecureSettings() error {
|
func (c *PostableUserConfig) EncryptSecureSettings() error {
|
||||||
for _, r := range c.AlertmanagerConfig.Receivers {
|
for _, r := range c.AlertmanagerConfig.Receivers {
|
||||||
for _, gr := range r.GrafanaManagedReceivers {
|
for _, gr := range r.GrafanaManagedReceivers {
|
||||||
|
|||||||
@@ -61,6 +61,25 @@ func (m *migration) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) {
|
|||||||
return &dashboard, nil
|
return &dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns the folder of the given dashboard (if exists)
|
||||||
|
func (m *migration) getFolder(dash dashboard, da dashAlert) (dashboard, error) {
|
||||||
|
// get folder if exists
|
||||||
|
folder := dashboard{}
|
||||||
|
if dash.FolderId > 0 {
|
||||||
|
exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
|
||||||
|
if err != nil {
|
||||||
|
return folder, fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return folder, fmt.Errorf("folder with id %v not found", dash.FolderId)
|
||||||
|
}
|
||||||
|
if !folder.IsFolder {
|
||||||
|
return folder, fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return folder, nil
|
||||||
|
}
|
||||||
|
|
||||||
// based on sqlstore.saveDashboard()
|
// based on sqlstore.saveDashboard()
|
||||||
// it should be called from inside a transaction
|
// it should be called from inside a transaction
|
||||||
func (m *migration) createFolder(orgID int64, title string) (*dashboard, error) {
|
func (m *migration) createFolder(orgID int64, title string) (*dashboard, error) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
@@ -27,7 +28,7 @@ func (m *migration) addSilence(da dashAlert, rule *alertRule) error {
|
|||||||
return errors.New("failed to create uuid for silence")
|
return errors.New("failed to create uuid for silence")
|
||||||
}
|
}
|
||||||
|
|
||||||
n, v := getLabelForRouteMatching(rule.Uid)
|
n, v := getLabelForRouteMatching(rule.UID)
|
||||||
s := &pb.MeshSilence{
|
s := &pb.MeshSilence{
|
||||||
Silence: &pb.Silence{
|
Silence: &pb.Silence{
|
||||||
Id: uid.String(),
|
Id: uid.String(),
|
||||||
@@ -50,7 +51,7 @@ func (m *migration) addSilence(da dashAlert, rule *alertRule) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) writeSilencesFile() error {
|
func (m *migration) writeSilencesFile(orgID int64) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for _, e := range m.silences {
|
for _, e := range m.silences {
|
||||||
if _, err := pbutil.WriteDelimited(&buf, e); err != nil {
|
if _, err := pbutil.WriteDelimited(&buf, e); err != nil {
|
||||||
@@ -58,7 +59,7 @@ func (m *migration) writeSilencesFile() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := openReplace(silencesFileName(m.mg))
|
f, err := openReplace(silencesFileNameForOrg(m.mg, orgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -70,8 +71,12 @@ func (m *migration) writeSilencesFile() error {
|
|||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func silencesFileName(mg *migrator.Migrator) string {
|
func getSilenceFileNamesForAllOrgs(mg *migrator.Migrator) ([]string, error) {
|
||||||
return filepath.Join(mg.Cfg.DataPath, "alerting", "silences")
|
return filepath.Glob(filepath.Join(mg.Cfg.DataPath, "alerting", "*", "silences"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func silencesFileNameForOrg(mg *migrator.Migrator, orgID int64) string {
|
||||||
|
return filepath.Join(mg.Cfg.DataPath, "alerting", strconv.Itoa(int(orgID)), "silences")
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceFile wraps a file that is moved to another filename on closing.
|
// replaceFile wraps a file that is moved to another filename on closing.
|
||||||
@@ -94,6 +99,10 @@ func (f *replaceFile) Close() error {
|
|||||||
func openReplace(filename string) (*replaceFile, error) {
|
func openReplace(filename string) (*replaceFile, error) {
|
||||||
tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63()))
|
tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63()))
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(tmpFilename), os.ModePerm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
f, err := os.Create(tmpFilename)
|
f, err := os.Create(tmpFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -267,6 +267,15 @@ func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) {
|
|||||||
|
|
||||||
mg.AddMigration("alert alert_configuration alertmanager_configuration column from TEXT to MEDIUMTEXT if mysql", migrator.NewRawSQLMigration("").
|
mg.AddMigration("alert alert_configuration alertmanager_configuration column from TEXT to MEDIUMTEXT if mysql", migrator.NewRawSQLMigration("").
|
||||||
Mysql("ALTER TABLE alert_configuration MODIFY alertmanager_configuration MEDIUMTEXT;"))
|
Mysql("ALTER TABLE alert_configuration MODIFY alertmanager_configuration MEDIUMTEXT;"))
|
||||||
|
|
||||||
|
mg.AddMigration("add column org_id in alert_configuration", migrator.NewAddColumnMigration(alertConfiguration, &migrator.Column{
|
||||||
|
Name: "org_id", Type: migrator.DB_BigInt, Nullable: false, Default: "0",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add index on org_id
|
||||||
|
mg.AddMigration("add index in alert_configuration table on org_id column", migrator.NewAddIndexMigration(alertConfiguration, &migrator.Index{
|
||||||
|
Cols: []string{"org_id"},
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {
|
func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ var migTitle = "move dashboard alerts to unified alerting"
|
|||||||
|
|
||||||
var rmMigTitle = "remove unified alerting data"
|
var rmMigTitle = "remove unified alerting data"
|
||||||
|
|
||||||
|
const clearMigrationEntryTitle = "clear migration entry %q"
|
||||||
|
|
||||||
type MigrationError struct {
|
type MigrationError struct {
|
||||||
AlertId int64
|
AlertId int64
|
||||||
Err error
|
Err error
|
||||||
@@ -49,19 +51,23 @@ func AddDashAlertMigration(mg *migrator.Migrator) {
|
|||||||
case ngEnabled && !migrationRun:
|
case ngEnabled && !migrationRun:
|
||||||
// Remove the migration entry that removes all unified alerting data. This is so when the feature
|
// Remove the migration entry that removes all unified alerting data. This is so when the feature
|
||||||
// flag is removed in future the "remove unified alerting data" migration will be run again.
|
// flag is removed in future the "remove unified alerting data" migration will be run again.
|
||||||
err = mg.ClearMigrationEntry(rmMigTitle)
|
mg.AddMigration(fmt.Sprintf(clearMigrationEntryTitle, rmMigTitle), &clearMigrationEntry{
|
||||||
|
migrationID: rmMigTitle,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mg.Logger.Error("alert migration error: could not clear alert migration for removing data", "error", err)
|
mg.Logger.Error("alert migration error: could not clear alert migration for removing data", "error", err)
|
||||||
}
|
}
|
||||||
mg.AddMigration(migTitle, &migration{
|
mg.AddMigration(migTitle, &migration{
|
||||||
seenChannelUIDs: make(map[string]struct{}),
|
seenChannelUIDs: make(map[string]struct{}),
|
||||||
migratedChannels: make(map[*notificationChannel]struct{}),
|
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
|
||||||
portedChannelGroups: make(map[string]string),
|
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
|
||||||
})
|
})
|
||||||
case !ngEnabled && migrationRun:
|
case !ngEnabled && migrationRun:
|
||||||
// Remove the migration entry that creates unified alerting data. This is so when the feature
|
// Remove the migration entry that creates unified alerting data. This is so when the feature
|
||||||
// flag is enabled in the future the migration "move dashboard alerts to unified alerting" will be run again.
|
// flag is enabled in the future the migration "move dashboard alerts to unified alerting" will be run again.
|
||||||
err = mg.ClearMigrationEntry(migTitle)
|
mg.AddMigration(fmt.Sprintf(clearMigrationEntryTitle, migTitle), &clearMigrationEntry{
|
||||||
|
migrationID: migTitle,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mg.Logger.Error("alert migration error: could not clear dashboard alert migration", "error", err)
|
mg.Logger.Error("alert migration error: could not clear dashboard alert migration", "error", err)
|
||||||
}
|
}
|
||||||
@@ -69,17 +75,84 @@ func AddDashAlertMigration(mg *migrator.Migrator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RerunDashAlertMigration force the dashboard alert migration to run
|
||||||
|
// to make sure that the Alertmanager configurations will be created for each organisation
|
||||||
|
func RerunDashAlertMigration(mg *migrator.Migrator) {
|
||||||
|
logs, err := mg.GetMigrationLog()
|
||||||
|
if err != nil {
|
||||||
|
mg.Logger.Crit("alert migration failure: could not get migration log", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneMigTitle := fmt.Sprintf("clone %s", migTitle)
|
||||||
|
cloneRmMigTitle := fmt.Sprintf("clone %s", rmMigTitle)
|
||||||
|
|
||||||
|
_, migrationRun := logs[cloneMigTitle]
|
||||||
|
|
||||||
|
ngEnabled := mg.Cfg.IsNgAlertEnabled()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ngEnabled && !migrationRun:
|
||||||
|
// Removes all unified alerting data. It is not recorded so when the feature
|
||||||
|
// flag is removed in future the "clone remove unified alerting data" migration will be run again.
|
||||||
|
mg.AddMigration(cloneRmMigTitle, &rmMigrationWithoutLogging{})
|
||||||
|
|
||||||
|
mg.AddMigration(cloneMigTitle, &migration{
|
||||||
|
seenChannelUIDs: make(map[string]struct{}),
|
||||||
|
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
|
||||||
|
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
|
||||||
|
})
|
||||||
|
|
||||||
|
case !ngEnabled && migrationRun:
|
||||||
|
// Remove the migration entry that creates unified alerting data. This is so when the feature
|
||||||
|
// flag is enabled in the future the migration "move dashboard alerts to unified alerting" will be run again.
|
||||||
|
mg.AddMigration(fmt.Sprintf(clearMigrationEntryTitle, cloneMigTitle), &clearMigrationEntry{
|
||||||
|
migrationID: cloneMigTitle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
mg.Logger.Error("alert migration error: could not clear clone dashboard alert migration", "error", err)
|
||||||
|
}
|
||||||
|
// Removes all unified alerting data. It is not recorded so when the feature
|
||||||
|
// flag is enabled in future the "clone remove unified alerting data" migration will be run again.
|
||||||
|
mg.AddMigration(cloneRmMigTitle, &rmMigrationWithoutLogging{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearMigrationEntry removes an entry fromt the migration_log table.
|
||||||
|
// This migration is not recorded in the migration_log so that it can re-run several times.
|
||||||
|
type clearMigrationEntry struct {
|
||||||
|
migrator.MigrationBase
|
||||||
|
|
||||||
|
migrationID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *clearMigrationEntry) SQL(dialect migrator.Dialect) string {
|
||||||
|
return "clear migration entry code migration"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *clearMigrationEntry) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
||||||
|
_, err := sess.SQL(`DELETE from migration_log where migration_id = ?`, m.migrationID).Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to clear migration entry %v: %w", m.migrationID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *clearMigrationEntry) SkipMigrationLog() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type migration struct {
|
type migration struct {
|
||||||
migrator.MigrationBase
|
migrator.MigrationBase
|
||||||
// session and mg are attached for convenience.
|
// session and mg are attached for convenience.
|
||||||
sess *xorm.Session
|
sess *xorm.Session
|
||||||
mg *migrator.Migrator
|
mg *migrator.Migrator
|
||||||
|
|
||||||
seenChannelUIDs map[string]struct{}
|
seenChannelUIDs map[string]struct{}
|
||||||
migratedChannels map[*notificationChannel]struct{}
|
migratedChannelsPerOrg map[int64]map[*notificationChannel]struct{}
|
||||||
silences []*pb.MeshSilence
|
silences []*pb.MeshSilence
|
||||||
portedChannelGroups map[string]string // Channel group key -> receiver name.
|
portedChannelGroupsPerOrg map[int64]map[string]string // Org -> Channel group key -> receiver name.
|
||||||
lastReceiverID int // For the auto generated receivers.
|
lastReceiverID int // For the auto generated receivers.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) SQL(dialect migrator.Dialect) string {
|
func (m *migration) SQL(dialect migrator.Dialect) string {
|
||||||
@@ -108,13 +181,13 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// allChannels: channelUID -> channelConfig
|
// allChannels: channelUID -> channelConfig
|
||||||
allChannels, defaultChannels, err := m.getNotificationChannelMap()
|
allChannelsPerOrg, defaultChannelsPerOrg, err := m.getNotificationChannelMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
amConfig := PostableUserConfig{}
|
amConfigPerOrg := make(amConfigsPerOrg, len(allChannelsPerOrg))
|
||||||
err = m.addDefaultChannels(&amConfig, allChannels, defaultChannels)
|
err = m.addDefaultChannels(amConfigPerOrg, allChannelsPerOrg, defaultChannelsPerOrg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -144,26 +217,11 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get folder if exists
|
// get folder if exists
|
||||||
folder := dashboard{}
|
folder, err := m.getFolder(dash, da)
|
||||||
if dash.FolderId > 0 {
|
if err != nil {
|
||||||
exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
|
return MigrationError{
|
||||||
if err != nil {
|
Err: err,
|
||||||
return MigrationError{
|
AlertId: da.Id,
|
||||||
Err: fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err),
|
|
||||||
AlertId: da.Id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return MigrationError{
|
|
||||||
Err: fmt.Errorf("folder with id %v not found", dash.FolderId),
|
|
||||||
AlertId: da.Id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !folder.IsFolder {
|
|
||||||
return MigrationError{
|
|
||||||
Err: fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId),
|
|
||||||
AlertId: da.Id,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,8 +278,12 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.updateReceiverAndRoute(allChannels, defaultChannels, da, rule, &amConfig); err != nil {
|
if _, ok := amConfigPerOrg[rule.OrgID]; !ok {
|
||||||
return err
|
m.mg.Logger.Info("no configuration found", "org", rule.OrgID)
|
||||||
|
} else {
|
||||||
|
if err := m.updateReceiverAndRoute(allChannelsPerOrg, defaultChannelsPerOrg, da, rule, amConfigPerOrg[rule.OrgID]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(mg.Dialect.DriverName(), migrator.Postgres) {
|
if strings.HasPrefix(mg.Dialect.DriverName(), migrator.Postgres) {
|
||||||
@@ -234,8 +296,8 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO better error handling, if constraint
|
// TODO better error handling, if constraint
|
||||||
rule.Title += fmt.Sprintf(" %v", rule.Uid)
|
rule.Title += fmt.Sprintf(" %v", rule.UID)
|
||||||
rule.RuleGroup += fmt.Sprintf(" %v", rule.Uid)
|
rule.RuleGroup += fmt.Sprintf(" %v", rule.UID)
|
||||||
|
|
||||||
_, err = m.sess.Insert(rule)
|
_, err = m.sess.Insert(rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -250,24 +312,26 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a separate receiver for all the unmigrated channels.
|
for orgID, amConfig := range amConfigPerOrg {
|
||||||
err = m.addUnmigratedChannels(&amConfig, allChannels, defaultChannels)
|
// Create a separate receiver for all the unmigrated channels.
|
||||||
if err != nil {
|
err = m.addUnmigratedChannels(orgID, amConfig, allChannelsPerOrg[orgID], defaultChannelsPerOrg[orgID])
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.writeAlertmanagerConfig(&amConfig, allChannels); err != nil {
|
if err := m.writeAlertmanagerConfig(orgID, amConfig, allChannelsPerOrg[orgID]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.writeSilencesFile(); err != nil {
|
if err := m.writeSilencesFile(orgID); err != nil {
|
||||||
m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err)
|
m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) writeAlertmanagerConfig(amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel) error {
|
func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel) error {
|
||||||
if len(allChannels) == 0 {
|
if len(allChannels) == 0 {
|
||||||
// No channels, hence don't require Alertmanager config.
|
// No channels, hence don't require Alertmanager config.
|
||||||
m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config")
|
m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config")
|
||||||
@@ -288,6 +352,7 @@ func (m *migration) writeAlertmanagerConfig(amConfig *PostableUserConfig, allCha
|
|||||||
// Since we are migration for a snapshot of the code, it is always going to migrate to
|
// Since we are migration for a snapshot of the code, it is always going to migrate to
|
||||||
// the v1 config.
|
// the v1 config.
|
||||||
ConfigurationVersion: "v1",
|
ConfigurationVersion: "v1",
|
||||||
|
OrgID: orgID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -297,13 +362,15 @@ func (m *migration) writeAlertmanagerConfig(amConfig *PostableUserConfig, allCha
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertConfiguration struct {
|
type AlertConfiguration struct {
|
||||||
ID int64 `xorm:"pk autoincr 'id'"`
|
ID int64 `xorm:"pk autoincr 'id'"`
|
||||||
|
OrgID int64 `xorm:"org_id"`
|
||||||
|
|
||||||
AlertmanagerConfiguration string
|
AlertmanagerConfiguration string
|
||||||
ConfigurationVersion string
|
ConfigurationVersion string
|
||||||
CreatedAt int64 `xorm:"created"`
|
CreatedAt int64 `xorm:"created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rmMigration removes Grafana 8 alert data
|
||||||
type rmMigration struct {
|
type rmMigration struct {
|
||||||
migrator.MigrationBase
|
migrator.MigrationBase
|
||||||
}
|
}
|
||||||
@@ -343,9 +410,23 @@ func (m *rmMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(silencesFileName(mg)); err != nil {
|
files, err := getSilenceFileNamesForAllOrgs(mg)
|
||||||
mg.Logger.Error("alert migration error: failed to remove silence file", "err", err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
if err := os.Remove(f); err != nil {
|
||||||
|
mg.Logger.Error("alert migration error: failed to remove silence file", "file", f, "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rmMigrationWithoutLogging is similar migration to rmMigration
|
||||||
|
// but is not recorded in the migration_log table so that it can rerun in the future
|
||||||
|
type rmMigrationWithoutLogging = rmMigration
|
||||||
|
|
||||||
|
func (m *rmMigrationWithoutLogging) SkipMigrationLog() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ func (m *MigrationBase) GetCondition() MigrationCondition {
|
|||||||
return m.Condition
|
return m.Condition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MigrationBase) SkipMigrationLog() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type RawSQLMigration struct {
|
type RawSQLMigration struct {
|
||||||
MigrationBase
|
MigrationBase
|
||||||
|
|
||||||
|
|||||||
@@ -108,13 +108,17 @@ func (mg *Migrator) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
|
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
|
||||||
record.Error = err.Error()
|
record.Error = err.Error()
|
||||||
if _, err := sess.Insert(&record); err != nil {
|
if !m.SkipMigrationLog() {
|
||||||
return err
|
if _, err := sess.Insert(&record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
record.Success = true
|
record.Success = true
|
||||||
_, err = sess.Insert(&record)
|
if !m.SkipMigrationLog() {
|
||||||
|
_, err = sess.Insert(&record)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
migrationsPerformed++
|
migrationsPerformed++
|
||||||
}
|
}
|
||||||
@@ -171,16 +175,6 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mg *Migrator) ClearMigrationEntry(id string) error {
|
|
||||||
sess := mg.x.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
_, err := sess.SQL(`DELETE from migration_log where migration_id = ?`, id).Query()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to clear migration entry %v: %w", id, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type dbTransactionFunc func(sess *xorm.Session) error
|
type dbTransactionFunc func(sess *xorm.Session) error
|
||||||
|
|
||||||
func (mg *Migrator) InTransaction(callback dbTransactionFunc) error {
|
func (mg *Migrator) InTransaction(callback dbTransactionFunc) error {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ type Migration interface {
|
|||||||
Id() string
|
Id() string
|
||||||
SetId(string)
|
SetId(string)
|
||||||
GetCondition() MigrationCondition
|
GetCondition() MigrationCondition
|
||||||
|
// SkipMigrationLog is used by dashboard alert migration to Grafana 8 Alerts
|
||||||
|
// for skipping recording it in the migration_log so that it can run several times.
|
||||||
|
// For all the other migrations it should be false.
|
||||||
|
SkipMigrationLog() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeMigration interface {
|
type CodeMigration interface {
|
||||||
|
|||||||
@@ -405,6 +405,9 @@ type Cfg struct {
|
|||||||
// Geomap base layer config
|
// Geomap base layer config
|
||||||
GeomapDefaultBaseLayerConfig map[string]interface{}
|
GeomapDefaultBaseLayerConfig map[string]interface{}
|
||||||
GeomapEnableCustomBaseLayers bool
|
GeomapEnableCustomBaseLayers bool
|
||||||
|
|
||||||
|
// Unified Alerting
|
||||||
|
AdminConfigPollInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
||||||
@@ -898,6 +901,10 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := cfg.readUnifiedAlertingSettings(iniFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
explore := iniFile.Section("explore")
|
explore := iniFile.Section("explore")
|
||||||
ExploreEnabled = explore.Key("enabled").MustBool(true)
|
ExploreEnabled = explore.Key("enabled").MustBool(true)
|
||||||
|
|
||||||
@@ -1349,6 +1356,13 @@ func readRenderingSettings(iniFile *ini.File, cfg *Cfg) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Cfg) readUnifiedAlertingSettings(iniFile *ini.File) error {
|
||||||
|
ua := iniFile.Section("unified_alerting")
|
||||||
|
s := ua.Key("admin_config_poll_interval_seconds").MustInt(60)
|
||||||
|
cfg.AdminConfigPollInterval = time.Second * time.Duration(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func readAlertingSettings(iniFile *ini.File) error {
|
func readAlertingSettings(iniFile *ini.File) error {
|
||||||
alerting := iniFile.Section("alerting")
|
alerting := iniFile.Section("alerting")
|
||||||
AlertingEnabled = alerting.Key("enabled").MustBool(true)
|
AlertingEnabled = alerting.Key("enabled").MustBool(true)
|
||||||
|
|||||||
@@ -31,28 +31,39 @@ func normalizeAzureCloud(cloudName string) string {
|
|||||||
switch strings.ToLower(cloudName) {
|
switch strings.ToLower(cloudName) {
|
||||||
// Public
|
// Public
|
||||||
case "azurecloud":
|
case "azurecloud":
|
||||||
|
fallthrough
|
||||||
case "azurepublic":
|
case "azurepublic":
|
||||||
|
fallthrough
|
||||||
case "azurepubliccloud":
|
case "azurepubliccloud":
|
||||||
|
fallthrough
|
||||||
case "public":
|
case "public":
|
||||||
return AzurePublic
|
return AzurePublic
|
||||||
|
|
||||||
// China
|
// China
|
||||||
case "azurechina":
|
case "azurechina":
|
||||||
|
fallthrough
|
||||||
case "azurechinacloud":
|
case "azurechinacloud":
|
||||||
|
fallthrough
|
||||||
case "china":
|
case "china":
|
||||||
return AzureChina
|
return AzureChina
|
||||||
|
|
||||||
// US Government
|
// US Government
|
||||||
case "azureusgovernment":
|
case "azureusgovernment":
|
||||||
|
fallthrough
|
||||||
case "azureusgovernmentcloud":
|
case "azureusgovernmentcloud":
|
||||||
|
fallthrough
|
||||||
case "usgov":
|
case "usgov":
|
||||||
|
fallthrough
|
||||||
case "usgovernment":
|
case "usgovernment":
|
||||||
return AzureUSGovernment
|
return AzureUSGovernment
|
||||||
|
|
||||||
// Germany
|
// Germany
|
||||||
case "azuregermancloud":
|
case "azuregermancloud":
|
||||||
|
fallthrough
|
||||||
case "azuregermany":
|
case "azuregermany":
|
||||||
|
fallthrough
|
||||||
case "german":
|
case "german":
|
||||||
|
fallthrough
|
||||||
case "germany":
|
case "germany":
|
||||||
return AzureGermany
|
return AzureGermany
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAzureSettings(t *testing.T) {
|
||||||
|
t.Run("cloud name", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
configuredValue string
|
||||||
|
resolvedValue string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should be Public if not set",
|
||||||
|
configuredValue: "",
|
||||||
|
resolvedValue: AzurePublic,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should be Public if set to Public",
|
||||||
|
configuredValue: AzurePublic,
|
||||||
|
resolvedValue: AzurePublic,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should be Public if set to Public using alternative name",
|
||||||
|
configuredValue: "AzurePublicCloud",
|
||||||
|
resolvedValue: AzurePublic,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should be China if set to China",
|
||||||
|
configuredValue: AzureChina,
|
||||||
|
resolvedValue: AzureChina,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should be US Government if set to US Government using alternative name",
|
||||||
|
configuredValue: "usgov",
|
||||||
|
resolvedValue: AzureUSGovernment,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should be same as set if not known",
|
||||||
|
configuredValue: "Custom123",
|
||||||
|
resolvedValue: "Custom123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range testCases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
cfg := NewCfg()
|
||||||
|
|
||||||
|
azureSection, err := cfg.Raw.NewSection("azure")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = azureSection.NewKey("cloud", c.configuredValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg.readAzureSettings()
|
||||||
|
require.NotNil(t, cfg.Azure)
|
||||||
|
|
||||||
|
assert.Equal(t, c.resolvedValue, cfg.Azure.Cloud)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package alerting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
||||||
|
|
||||||
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdminConfiguration_SendingToExternalAlertmanagers(t *testing.T) {
|
||||||
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||||
|
EnableFeatureToggles: []string{"ngalert"},
|
||||||
|
DisableAnonymous: true,
|
||||||
|
NGAlertAdminConfigIntervalSeconds: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
s := testinfra.SetUpDatabase(t, dir)
|
||||||
|
// override bus to get the GetSignedInUserQuery handler
|
||||||
|
s.Bus = bus.GetBus()
|
||||||
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, s)
|
||||||
|
|
||||||
|
// Create a user to make authenticated requests
|
||||||
|
createUser(t, s, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_ADMIN),
|
||||||
|
Login: "grafana",
|
||||||
|
Password: "password",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a couple of "fake" Alertmanagers
|
||||||
|
fakeAM1 := schedule.NewFakeExternalAlertmanager(t)
|
||||||
|
fakeAM2 := schedule.NewFakeExternalAlertmanager(t)
|
||||||
|
|
||||||
|
// Now, let's test the configuration API.
|
||||||
|
{
|
||||||
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/v1/ngalert/admin_config", grafanaListedAddr)
|
||||||
|
resp := getRequest(t, alertsURL, http.StatusNotFound) // nolint
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, string(b), "{\"message\": \"no admin configuration available\"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, lets re-set external Alertmanagers.
|
||||||
|
{
|
||||||
|
ac := apimodels.PostableNGalertConfig{
|
||||||
|
Alertmanagers: []string{fakeAM1.URL(), fakeAM2.URL()},
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
err := enc.Encode(&ac)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/v1/ngalert/admin_config", grafanaListedAddr)
|
||||||
|
resp := postRequest(t, alertsURL, buf.String(), http.StatusCreated) // nolint
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, string(b), "{\"message\": \"admin configuration updated\"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get the configuration again, it shows us what we've set.
|
||||||
|
{
|
||||||
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/v1/ngalert/admin_config", grafanaListedAddr)
|
||||||
|
resp := getRequest(t, alertsURL, http.StatusOK) // nolint
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, string(b), fmt.Sprintf("{\"alertmanagers\":[\"%s\",\"%s\"]}\n", fakeAM1.URL(), fakeAM2.URL()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the configuration set, we should eventually discover those Alertmanagers set.
|
||||||
|
{
|
||||||
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/v1/ngalert/alertmanagers", grafanaListedAddr)
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
resp := getRequest(t, alertsURL, http.StatusOK) // nolint
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var alertmanagers apimodels.GettableAlertmanagers
|
||||||
|
require.NoError(t, json.Unmarshal(b, &alertmanagers))
|
||||||
|
|
||||||
|
return len(alertmanagers.Data.Active) == 2
|
||||||
|
}, 80*time.Second, 4*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, let's set an alert that should fire as quickly as possible.
|
||||||
|
{
|
||||||
|
// create the namespace we'll save our alerts to
|
||||||
|
_, err := createFolder(t, s, 0, "default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
interval, err := model.ParseDuration("10s")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rules := apimodels.PostableRuleGroupConfig{
|
||||||
|
Name: "arulegroup",
|
||||||
|
Interval: interval,
|
||||||
|
Rules: []apimodels.PostableExtendedRuleNode{
|
||||||
|
{
|
||||||
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
||||||
|
For: interval,
|
||||||
|
Labels: map[string]string{"label1": "val1"},
|
||||||
|
Annotations: map[string]string{"annotation1": "val1"},
|
||||||
|
},
|
||||||
|
// this rule does not explicitly set no data and error states
|
||||||
|
// therefore it should get the default values
|
||||||
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||||
|
Title: "AlwaysFiring",
|
||||||
|
Condition: "A",
|
||||||
|
Data: []ngmodels.AlertQuery{
|
||||||
|
{
|
||||||
|
RefID: "A",
|
||||||
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
||||||
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
||||||
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
||||||
|
},
|
||||||
|
DatasourceUID: "-100",
|
||||||
|
Model: json.RawMessage(`{
|
||||||
|
"type": "math",
|
||||||
|
"expression": "2 + 3 > 1"
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
err = enc.Encode(&rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ruleURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
||||||
|
// nolint
|
||||||
|
_ = postRequest(t, ruleURL, buf.String(), http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Eventually, our Alertmanagers should receiver the alert.
|
||||||
|
{
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return fakeAM1.AlertsCount() == 1 && fakeAM2.AlertsCount() == 1
|
||||||
|
}, 60*time.Second, 5*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
@@ -16,12 +18,34 @@ import (
|
|||||||
func TestAlertmanagerConfigurationIsTransactional(t *testing.T) {
|
func TestAlertmanagerConfigurationIsTransactional(t *testing.T) {
|
||||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||||
EnableFeatureToggles: []string{"ngalert"},
|
EnableFeatureToggles: []string{"ngalert"},
|
||||||
AnonymousUserRole: models.ROLE_EDITOR,
|
DisableAnonymous: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
store := testinfra.SetUpDatabase(t, dir)
|
store := testinfra.SetUpDatabase(t, dir)
|
||||||
|
// override bus to get the GetSignedInUserQuery handler
|
||||||
|
store.Bus = bus.GetBus()
|
||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
alertConfigURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
|
||||||
|
// create user under main organisation
|
||||||
|
userID := createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "editor",
|
||||||
|
Login: "editor",
|
||||||
|
})
|
||||||
|
|
||||||
|
// create another organisation
|
||||||
|
orgID := createOrg(t, store, "another org", userID)
|
||||||
|
|
||||||
|
// create user under different organisation
|
||||||
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "editor-42",
|
||||||
|
Login: "editor-42",
|
||||||
|
OrgId: orgID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// editor from main organisation requests configuration
|
||||||
|
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
||||||
|
|
||||||
// On a blank start with no configuration, it saves and delivers the default configuration.
|
// On a blank start with no configuration, it saves and delivers the default configuration.
|
||||||
{
|
{
|
||||||
@@ -66,17 +90,48 @@ func TestAlertmanagerConfigurationIsTransactional(t *testing.T) {
|
|||||||
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
||||||
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
|
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editor42 from organisation 42 posts configuration
|
||||||
|
alertConfigURL = fmt.Sprintf("http://editor-42:editor-42@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
||||||
|
|
||||||
|
// Post the alertmanager config.
|
||||||
|
{
|
||||||
|
mockChannel := newMockNotificationChannel(t, grafanaListedAddr)
|
||||||
|
amConfig := getAlertmanagerConfig(mockChannel.server.Addr)
|
||||||
|
postRequest(t, alertConfigURL, amConfig, http.StatusAccepted) // nolint
|
||||||
|
|
||||||
|
// Verifying that the new configuration is returned
|
||||||
|
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
||||||
|
b := getBody(t, resp.Body)
|
||||||
|
re := regexp.MustCompile(`"uid":"([\w|-]*)"`)
|
||||||
|
e := getExpAlertmanagerConfigFromAPI(mockChannel.server.Addr)
|
||||||
|
require.JSONEq(t, e, string(re.ReplaceAll([]byte(b), []byte(`"uid":""`))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that main organisation still gets the default configuration
|
||||||
|
alertConfigURL = fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
||||||
|
{
|
||||||
|
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
||||||
|
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
||||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||||
EnableFeatureToggles: []string{"ngalert"},
|
EnableFeatureToggles: []string{"ngalert"},
|
||||||
AnonymousUserRole: models.ROLE_EDITOR,
|
DisableAnonymous: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
store := testinfra.SetUpDatabase(t, dir)
|
store := testinfra.SetUpDatabase(t, dir)
|
||||||
|
// override bus to get the GetSignedInUserQuery handler
|
||||||
|
store.Bus = bus.GetBus()
|
||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
alertConfigURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "editor",
|
||||||
|
Login: "editor",
|
||||||
|
})
|
||||||
|
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
||||||
generatedUID := ""
|
generatedUID := ""
|
||||||
|
|
||||||
// create a new configuration that has a secret
|
// create a new configuration that has a secret
|
||||||
|
|||||||
@@ -39,9 +39,21 @@ func TestAMConfigAccess(t *testing.T) {
|
|||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
// Create a users to make authenticated requests
|
// Create a users to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor"))
|
DefaultOrgRole: string(models.ROLE_VIEWER),
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin"))
|
Password: "viewer",
|
||||||
|
Login: "viewer",
|
||||||
|
})
|
||||||
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "editor",
|
||||||
|
Login: "editor",
|
||||||
|
})
|
||||||
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_ADMIN),
|
||||||
|
Password: "admin",
|
||||||
|
Login: "admin",
|
||||||
|
})
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
desc string
|
desc string
|
||||||
@@ -402,7 +414,11 @@ func TestAlertAndGroupsQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
// invalid credentials request to get the alerts should fail
|
// invalid credentials request to get the alerts should fail
|
||||||
{
|
{
|
||||||
@@ -554,9 +570,21 @@ func TestRulerAccess(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create a users to make authenticated requests
|
// Create a users to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor"))
|
DefaultOrgRole: string(models.ROLE_VIEWER),
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin"))
|
Password: "viewer",
|
||||||
|
Login: "viewer",
|
||||||
|
})
|
||||||
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "editor",
|
||||||
|
Login: "editor",
|
||||||
|
})
|
||||||
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_ADMIN),
|
||||||
|
Password: "admin",
|
||||||
|
Login: "admin",
|
||||||
|
})
|
||||||
|
|
||||||
// Now, let's test the access policies.
|
// Now, let's test the access policies.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -668,8 +696,16 @@ func TestDeleteFolderWithRules(t *testing.T) {
|
|||||||
namespaceUID, err := createFolder(t, store, 0, "default")
|
namespaceUID, err := createFolder(t, store, 0, "default")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor"))
|
DefaultOrgRole: string(models.ROLE_VIEWER),
|
||||||
|
Password: "viewer",
|
||||||
|
Login: "viewer",
|
||||||
|
})
|
||||||
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "editor",
|
||||||
|
Login: "editor",
|
||||||
|
})
|
||||||
|
|
||||||
createRule(t, grafanaListedAddr, "default", "editor", "editor")
|
createRule(t, grafanaListedAddr, "default", "editor", "editor")
|
||||||
|
|
||||||
@@ -815,12 +851,14 @@ func TestAlertRuleCRUD(t *testing.T) {
|
|||||||
store.Bus = bus.GetBus()
|
store.Bus = bus.GetBus()
|
||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
err := createUser(t, store, models.ROLE_EDITOR, "grafana", "password")
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
require.NoError(t, err)
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
// Create the namespace we'll save our alerts to.
|
// Create the namespace we'll save our alerts to.
|
||||||
_, err = createFolder(t, store, 0, "default")
|
_, err := createFolder(t, store, 0, "default")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
interval, err := model.ParseDuration("1m")
|
interval, err := model.ParseDuration("1m")
|
||||||
@@ -1827,7 +1865,11 @@ func TestQuota(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
interval, err := model.ParseDuration("1m")
|
interval, err := model.ParseDuration("1m")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1921,7 +1963,11 @@ func TestEval(t *testing.T) {
|
|||||||
store.Bus = bus.GetBus()
|
store.Bus = bus.GetBus()
|
||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
// Create the namespace we'll save our alerts to.
|
// Create the namespace we'll save our alerts to.
|
||||||
_, err := createFolder(t, store, 0, "default")
|
_, err := createFolder(t, store, 0, "default")
|
||||||
@@ -2338,16 +2384,18 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st
|
|||||||
return string(json), m
|
return string(json), m
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUser(t *testing.T, store *sqlstore.SQLStore, role models.RoleType, username, password string) error {
|
func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) int64 {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cmd := models.CreateUserCommand{
|
u, err := store.CreateUser(context.Background(), cmd)
|
||||||
Login: username,
|
require.NoError(t, err)
|
||||||
Password: password,
|
return u.Id
|
||||||
DefaultOrgRole: string(role),
|
}
|
||||||
}
|
|
||||||
_, err := store.CreateUser(context.Background(), cmd)
|
func createOrg(t *testing.T, store *sqlstore.SQLStore, name string, userID int64) int64 {
|
||||||
return err
|
org, err := store.CreateOrgWithMember(name, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return org.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLongString(t *testing.T, n int) string {
|
func getLongString(t *testing.T, n int) string {
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ func TestAvailableChannels(t *testing.T) {
|
|||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr)
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr)
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ func TestNotificationChannels(t *testing.T) {
|
|||||||
bus.AddHandlerCtx("", mockEmail.sendEmailCommandHandlerSync)
|
bus.AddHandlerCtx("", mockEmail.sendEmailCommandHandlerSync)
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, s, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, s, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
{
|
{
|
||||||
// There are no notification channel config initially - so it returns the default configuration.
|
// There are no notification channel config initially - so it returns the default configuration.
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ func TestPrometheusRules(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
interval, err := model.ParseDuration("10s")
|
interval, err := model.ParseDuration("10s")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -270,7 +274,11 @@ func TestPrometheusRulesPermissions(t *testing.T) {
|
|||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
// Create a namespace under default organisation (orgID = 1) where we'll save some alerts.
|
// Create a namespace under default organisation (orgID = 1) where we'll save some alerts.
|
||||||
_, err := createFolder(t, store, 0, "folder1")
|
_, err := createFolder(t, store, 0, "folder1")
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ func TestAlertRulePermissions(t *testing.T) {
|
|||||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR),
|
||||||
|
Password: "password",
|
||||||
|
Login: "grafana",
|
||||||
|
})
|
||||||
|
|
||||||
// Create the namespace we'll save our alerts to.
|
// Create the namespace we'll save our alerts to.
|
||||||
_, err := createFolder(t, store, 0, "folder1")
|
_, err := createFolder(t, store, 0, "folder1")
|
||||||
@@ -320,7 +324,11 @@ func TestAlertRuleConflictingTitle(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin"))
|
createUser(t, store, models.CreateUserCommand{
|
||||||
|
DefaultOrgRole: string(models.ROLE_ADMIN),
|
||||||
|
Password: "admin",
|
||||||
|
Login: "admin",
|
||||||
|
})
|
||||||
|
|
||||||
interval, err := model.ParseDuration("1m")
|
interval, err := model.ParseDuration("1m")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ func postRequest(t *testing.T, url string, body string, expStatusCode int) *http
|
|||||||
buf := bytes.NewReader([]byte(body))
|
buf := bytes.NewReader([]byte(body))
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
resp, err := http.Post(url, "application/json", buf)
|
resp, err := http.Post(url, "application/json", buf)
|
||||||
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, resp.Body.Close())
|
require.NoError(t, resp.Body.Close())
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
if expStatusCode != resp.StatusCode {
|
if expStatusCode != resp.StatusCode {
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -224,6 +224,13 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
|||||||
_, err = featureSection.NewKey("enable", strings.Join(o.EnableFeatureToggles, " "))
|
_, err = featureSection.NewKey("enable", strings.Join(o.EnableFeatureToggles, " "))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
if o.NGAlertAdminConfigIntervalSeconds != 0 {
|
||||||
|
ngalertingSection, err := cfg.NewSection("ngalerting")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = ngalertingSection.NewKey("admin_config_poll_interval_seconds", fmt.Sprintf("%d", o.NGAlertAdminConfigIntervalSeconds))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
if o.AnonymousUserRole != "" {
|
if o.AnonymousUserRole != "" {
|
||||||
_, err = anonSect.NewKey("org_role", string(o.AnonymousUserRole))
|
_, err = anonSect.NewKey("org_role", string(o.AnonymousUserRole))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -265,12 +272,13 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GrafanaOpts struct {
|
type GrafanaOpts struct {
|
||||||
EnableCSP bool
|
EnableCSP bool
|
||||||
EnableFeatureToggles []string
|
EnableFeatureToggles []string
|
||||||
AnonymousUserRole models.RoleType
|
NGAlertAdminConfigIntervalSeconds int
|
||||||
EnableQuota bool
|
AnonymousUserRole models.RoleType
|
||||||
DisableAnonymous bool
|
EnableQuota bool
|
||||||
CatalogAppEnabled bool
|
DisableAnonymous bool
|
||||||
ViewersCanEdit bool
|
CatalogAppEnabled bool
|
||||||
PluginAdminEnabled bool
|
ViewersCanEdit bool
|
||||||
|
PluginAdminEnabled bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,15 +179,38 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return dataResponseErrorWithExecuted(err)
|
return dataResponseErrorWithExecuted(err)
|
||||||
}
|
}
|
||||||
if frame.Meta == nil {
|
|
||||||
frame.Meta = &data.FrameMeta{}
|
|
||||||
}
|
|
||||||
frame.Meta.ExecutedQueryString = req.URL.RawQuery
|
|
||||||
|
|
||||||
dataResponse.Frames = data.Frames{frame}
|
azurePortalUrl, err := getAzurePortalUrl(dsInfo.Cloud)
|
||||||
|
if err != nil {
|
||||||
|
return dataResponseErrorWithExecuted(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := azurePortalUrl + "/#blade/HubsExtension/ArgQueryBlade/query/" + url.PathEscape(query.InterpolatedQuery)
|
||||||
|
frameWithLink := addConfigData(*frame, url)
|
||||||
|
if frameWithLink.Meta == nil {
|
||||||
|
frameWithLink.Meta = &data.FrameMeta{}
|
||||||
|
}
|
||||||
|
frameWithLink.Meta.ExecutedQueryString = req.URL.RawQuery
|
||||||
|
|
||||||
|
dataResponse.Frames = data.Frames{&frameWithLink}
|
||||||
return dataResponse
|
return dataResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addConfigData(frame data.Frame, dl string) data.Frame {
|
||||||
|
for i := range frame.Fields {
|
||||||
|
if frame.Fields[i].Config == nil {
|
||||||
|
frame.Fields[i].Config = &data.FieldConfig{}
|
||||||
|
}
|
||||||
|
deepLink := data.DataLink{
|
||||||
|
Title: "View in Azure Portal",
|
||||||
|
TargetBlank: true,
|
||||||
|
URL: dl,
|
||||||
|
}
|
||||||
|
frame.Fields[i].Config.Links = append(frame.Fields[i].Config.Links, deepLink)
|
||||||
|
}
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte, url string) (*http.Request, error) {
|
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte, url string) (*http.Request, error) {
|
||||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -228,3 +251,18 @@ func (e *AzureResourceGraphDatasource) unmarshalResponse(res *http.Response) (Az
|
|||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAzurePortalUrl(azureCloud string) (string, error) {
|
||||||
|
switch azureCloud {
|
||||||
|
case setting.AzurePublic:
|
||||||
|
return "https://portal.azure.com", nil
|
||||||
|
case setting.AzureChina:
|
||||||
|
return "https://portal.azure.cn", nil
|
||||||
|
case setting.AzureUSGovernment:
|
||||||
|
return "https://portal.azure.us", nil
|
||||||
|
case setting.AzureGermany:
|
||||||
|
return "https://portal.microsoftazure.de", nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("the cloud is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,3 +113,42 @@ func TestAzureResourceGraphCreateRequest(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddConfigData(t *testing.T) {
|
||||||
|
field := data.Field{}
|
||||||
|
dataLink := data.DataLink{Title: "View in Azure Portal", TargetBlank: true, URL: "http://ds"}
|
||||||
|
frame := data.Frame{
|
||||||
|
Fields: []*data.Field{&field},
|
||||||
|
}
|
||||||
|
frameWithLink := addConfigData(frame, "http://ds")
|
||||||
|
expectedFrameWithLink := data.Frame{
|
||||||
|
Fields: []*data.Field{
|
||||||
|
{
|
||||||
|
Config: &data.FieldConfig{
|
||||||
|
Links: []data.DataLink{dataLink},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !cmp.Equal(frameWithLink, expectedFrameWithLink, data.FrameTestCompareOptions()...) {
|
||||||
|
t.Errorf("unexpepcted frame: %v", cmp.Diff(frameWithLink, expectedFrameWithLink, data.FrameTestCompareOptions()...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAzurePortalUrl(t *testing.T) {
|
||||||
|
clouds := []string{setting.AzurePublic, setting.AzureChina, setting.AzureUSGovernment, setting.AzureGermany}
|
||||||
|
expectedAzurePortalUrl := map[string]interface{}{
|
||||||
|
setting.AzurePublic: "https://portal.azure.com",
|
||||||
|
setting.AzureChina: "https://portal.azure.cn",
|
||||||
|
setting.AzureUSGovernment: "https://portal.azure.us",
|
||||||
|
setting.AzureGermany: "https://portal.microsoftazure.de",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cloud := range clouds {
|
||||||
|
azurePortalUrl, err := getAzurePortalUrl(cloud)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("The cloud not supported")
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedAzurePortalUrl[cloud], azurePortalUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ var metricsMap = map[string][]string{
|
|||||||
"AWS/DAX": {"CPUUtilization", "NetworkPacketsIn", "NetworkPacketsOut", "GetItemRequestCount", "BatchGetItemRequestCount", "BatchWriteItemRequestCount", "DeleteItemRequestCount", "PutItemRequestCount", "UpdateItemRequestCount", "TransactWriteItemsCount", "TransactGetItemsCount", "ItemCacheHits", "ItemCacheMisses", "QueryCacheHits", "QueryCacheMisses", "ScanCacheHits", "ScanCacheMisses", "TotalRequestCount", "ErrorRequestCount", "FaultRequestCount", "FailedRequestCount", "QueryRequestCount", "ScanRequestCount", "ClientConnections", "EstimatedDbSize", "EvictedSize"},
|
"AWS/DAX": {"CPUUtilization", "NetworkPacketsIn", "NetworkPacketsOut", "GetItemRequestCount", "BatchGetItemRequestCount", "BatchWriteItemRequestCount", "DeleteItemRequestCount", "PutItemRequestCount", "UpdateItemRequestCount", "TransactWriteItemsCount", "TransactGetItemsCount", "ItemCacheHits", "ItemCacheMisses", "QueryCacheHits", "QueryCacheMisses", "ScanCacheHits", "ScanCacheMisses", "TotalRequestCount", "ErrorRequestCount", "FaultRequestCount", "FailedRequestCount", "QueryRequestCount", "ScanRequestCount", "ClientConnections", "EstimatedDbSize", "EvictedSize"},
|
||||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "TimeToLiveDeletedItemCount", "UserErrors", "WriteThrottleEvents"},
|
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "TimeToLiveDeletedItemCount", "UserErrors", "WriteThrottleEvents"},
|
||||||
"AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"},
|
"AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"},
|
||||||
"AWS/EC2": {"CPUCreditBalance", "CPUCreditUsage", "CPUSurplusCreditBalance", "CPUSurplusCreditsCharged", "CPUUtilization", "DiskReadBytes", "DiskReadOps", "DiskWriteBytes", "DiskWriteOps", "EBSByteBalance%", "EBSIOBalance%", "EBSReadBytes", "EBSReadOps", "EBSWriteBytes", "EBSWriteOps", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
"AWS/EC2": {"CPUCreditBalance", "CPUCreditUsage", "CPUSurplusCreditBalance", "CPUSurplusCreditsCharged", "CPUUtilization", "DiskReadBytes", "DiskReadOps", "DiskWriteBytes", "DiskWriteOps", "EBSByteBalance%", "EBSIOBalance%", "EBSReadBytes", "EBSReadOps", "EBSWriteBytes", "EBSWriteOps", "MetadataNoToken", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
||||||
"AWS/EC2/API": {"ClientErrors", "RequestLimitExceeded", "ServerErrors", "SuccessfulCalls"},
|
"AWS/EC2/API": {"ClientErrors", "RequestLimitExceeded", "ServerErrors", "SuccessfulCalls"},
|
||||||
"AWS/EC2CapacityReservations": {"AvailableInstanceCount", "InstanceUtilization", "TotalInstanceCount", "UsedInstanceCount"},
|
"AWS/EC2CapacityReservations": {"AvailableInstanceCount", "InstanceUtilization", "TotalInstanceCount", "UsedInstanceCount"},
|
||||||
"AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"},
|
"AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"},
|
||||||
|
|||||||
@@ -37,5 +37,13 @@ func (a ByTime) Less(i, j int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if timeField.At(i).(*time.Time) == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeField.At(j).(*time.Time) == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return (timeField.At(i).(*time.Time)).Before(*timeField.At(j).(*time.Time))
|
return (timeField.At(i).(*time.Time)).Before(*timeField.At(j).(*time.Time))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,46 +12,77 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFrameSort(t *testing.T) {
|
func TestFrameSort(t *testing.T) {
|
||||||
timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 17:04:05.000")
|
t.Run("sort simple frame", func(t *testing.T) {
|
||||||
timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 16:04:05.000")
|
timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 17:04:05.000")
|
||||||
timeC, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 15:04:05.000")
|
timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 16:04:05.000")
|
||||||
timeVals := []*time.Time{
|
timeC, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 15:04:05.000")
|
||||||
&timeA, &timeB, &timeC,
|
timeVals := []*time.Time{
|
||||||
}
|
&timeA, &timeB, &timeC,
|
||||||
timeField := data.NewField("@timestamp", nil, timeVals)
|
}
|
||||||
|
timeField := data.NewField("@timestamp", nil, timeVals)
|
||||||
|
|
||||||
stringField := data.NewField("line", nil, []*string{
|
stringField := data.NewField("line", nil, []*string{
|
||||||
aws.String("test message 1"),
|
aws.String("test message 1"),
|
||||||
aws.String("test message 2"),
|
aws.String("test message 2"),
|
||||||
aws.String("test message 3"),
|
aws.String("test message 3"),
|
||||||
|
})
|
||||||
|
|
||||||
|
numberField := data.NewField("nums", nil, []*float64{
|
||||||
|
aws.Float64(20.0),
|
||||||
|
aws.Float64(50.0),
|
||||||
|
aws.Float64(17.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
expectedDataframe := &data.Frame{
|
||||||
|
Name: "CloudWatchLogsResponse",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
timeField,
|
||||||
|
stringField,
|
||||||
|
numberField,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(ByTime(*expectedDataframe))
|
||||||
|
|
||||||
|
for i := 1; i < timeField.Len(); i++ {
|
||||||
|
assert.True(t, timeField.At(i).(*time.Time).After(*(timeField.At(i - 1).(*time.Time))))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, *stringField.At(0).(*string), "test message 3")
|
||||||
|
assert.Equal(t, *stringField.At(1).(*string), "test message 2")
|
||||||
|
assert.Equal(t, *stringField.At(2).(*string), "test message 1")
|
||||||
|
|
||||||
|
assert.Equal(t, *numberField.At(0).(*float64), 17.0)
|
||||||
|
assert.Equal(t, *numberField.At(1).(*float64), 50.0)
|
||||||
|
assert.Equal(t, *numberField.At(2).(*float64), 20.0)
|
||||||
})
|
})
|
||||||
|
|
||||||
numberField := data.NewField("nums", nil, []*float64{
|
t.Run("sort with nil", func(t *testing.T) {
|
||||||
aws.Float64(20.0),
|
timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 17:04:05.000")
|
||||||
aws.Float64(50.0),
|
timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 16:04:05.000")
|
||||||
aws.Float64(17.0),
|
timeVals := []*time.Time{
|
||||||
|
&timeA, &timeB, nil,
|
||||||
|
}
|
||||||
|
timeField := data.NewField("@timestamp", nil, timeVals)
|
||||||
|
|
||||||
|
stringField := data.NewField("line", nil, []*string{
|
||||||
|
aws.String("test message 1"),
|
||||||
|
aws.String("test message 2"),
|
||||||
|
aws.String("test message 3"),
|
||||||
|
})
|
||||||
|
|
||||||
|
frame := &data.Frame{
|
||||||
|
Name: "CloudWatchLogsResponse",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
timeField,
|
||||||
|
stringField,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(ByTime(*frame))
|
||||||
|
|
||||||
|
assert.Equal(t, *stringField.At(0).(*string), "test message 2")
|
||||||
|
assert.Equal(t, *stringField.At(1).(*string), "test message 1")
|
||||||
|
assert.Equal(t, *stringField.At(2).(*string), "test message 3")
|
||||||
})
|
})
|
||||||
|
|
||||||
expectedDataframe := &data.Frame{
|
|
||||||
Name: "CloudWatchLogsResponse",
|
|
||||||
Fields: []*data.Field{
|
|
||||||
timeField,
|
|
||||||
stringField,
|
|
||||||
numberField,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(ByTime(*expectedDataframe))
|
|
||||||
|
|
||||||
for i := 1; i < timeField.Len(); i++ {
|
|
||||||
assert.True(t, timeField.At(i).(*time.Time).After(*(timeField.At(i - 1).(*time.Time))))
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, *stringField.At(0).(*string), "test message 3")
|
|
||||||
assert.Equal(t, *stringField.At(1).(*string), "test message 2")
|
|
||||||
assert.Equal(t, *stringField.At(2).(*string), "test message 1")
|
|
||||||
|
|
||||||
assert.Equal(t, *numberField.At(0).(*float64), 17.0)
|
|
||||||
assert.Equal(t, *numberField.At(1).(*float64), 50.0)
|
|
||||||
assert.Equal(t, *numberField.At(2).(*float64), 20.0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type DatasourceInfo struct {
|
|||||||
HTTPClientOpts sdkhttpclient.Options
|
HTTPClientOpts sdkhttpclient.Options
|
||||||
URL string
|
URL string
|
||||||
HTTPMethod string
|
HTTPMethod string
|
||||||
|
TimeInterval string
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -84,11 +85,26 @@ func newInstanceSettings() datasource.InstanceFactoryFunc {
|
|||||||
return nil, errors.New("no http method provided")
|
return nil, errors.New("no http method provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// timeInterval can be a string or can be missing.
|
||||||
|
// if it is missing, we set it to empty-string
|
||||||
|
|
||||||
|
timeInterval := ""
|
||||||
|
|
||||||
|
timeIntervalJson := jsonData["timeInterval"]
|
||||||
|
if timeIntervalJson != nil {
|
||||||
|
// if it is not nil, it must be a string
|
||||||
|
timeInterval, ok = timeIntervalJson.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid time-interval provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mdl := DatasourceInfo{
|
mdl := DatasourceInfo{
|
||||||
ID: settings.ID,
|
ID: settings.ID,
|
||||||
URL: settings.URL,
|
URL: settings.URL,
|
||||||
HTTPClientOpts: httpCliOpts,
|
HTTPClientOpts: httpCliOpts,
|
||||||
HTTPMethod: httpMethod,
|
HTTPMethod: httpMethod,
|
||||||
|
TimeInterval: timeInterval,
|
||||||
}
|
}
|
||||||
return mdl, nil
|
return mdl, nil
|
||||||
}
|
}
|
||||||
@@ -123,7 +139,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
Responses: backend.Responses{},
|
Responses: backend.Responses{},
|
||||||
}
|
}
|
||||||
|
|
||||||
queries, err := s.parseQuery(req.Queries)
|
queries, err := s.parseQuery(req.Queries, dsInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
@@ -217,7 +233,7 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string {
|
|||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) parseQuery(queries []backend.DataQuery) (
|
func (s *Service) parseQuery(queries []backend.DataQuery, dsInfo *DatasourceInfo) (
|
||||||
[]*PrometheusQuery, error) {
|
[]*PrometheusQuery, error) {
|
||||||
var intervalMode string
|
var intervalMode string
|
||||||
var adjustedInterval time.Duration
|
var adjustedInterval time.Duration
|
||||||
@@ -239,7 +255,7 @@ func (s *Service) parseQuery(queries []backend.DataQuery) (
|
|||||||
end := queryModel.TimeRange.To
|
end := queryModel.TimeRange.To
|
||||||
queryInterval := jsonModel.Get("interval").MustString("")
|
queryInterval := jsonModel.Get("interval").MustString("")
|
||||||
|
|
||||||
dsInterval, err := tsdb.GetIntervalFrom(queryInterval, "", 0, 15*time.Second)
|
foundInterval, err := tsdb.GetIntervalFrom(dsInfo.TimeInterval, queryInterval, 0, 15*time.Second)
|
||||||
hasQueryInterval := queryInterval != ""
|
hasQueryInterval := queryInterval != ""
|
||||||
// Only use stepMode if we have interval in query, otherwise use "min"
|
// Only use stepMode if we have interval in query, otherwise use "min"
|
||||||
if hasQueryInterval {
|
if hasQueryInterval {
|
||||||
@@ -253,7 +269,7 @@ func (s *Service) parseQuery(queries []backend.DataQuery) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatedInterval, err := s.intervalCalculator.Calculate(queries[0].TimeRange, dsInterval, tsdb.IntervalMode(intervalMode))
|
calculatedInterval, err := s.intervalCalculator.Calculate(queries[0].TimeRange, foundInterval, tsdb.IntervalMode(intervalMode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
To: now.Add(12 * time.Hour),
|
To: now.Add(12 * time.Hour),
|
||||||
}
|
}
|
||||||
query.TimeRange = timeRange
|
query.TimeRange = timeRange
|
||||||
models, err := service.parseQuery([]backend.DataQuery{query})
|
models, err := service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Second*30, models[0].Step)
|
require.Equal(t, time.Second*30, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -74,7 +74,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
To: now.Add(12 * time.Hour),
|
To: now.Add(12 * time.Hour),
|
||||||
}
|
}
|
||||||
query.TimeRange = timeRange
|
query.TimeRange = timeRange
|
||||||
models, err := service.parseQuery([]backend.DataQuery{query})
|
models, err := service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Second*7, models[0].Step)
|
require.Equal(t, time.Second*7, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -92,7 +92,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
To: now.Add(12 * time.Hour),
|
To: now.Add(12 * time.Hour),
|
||||||
}
|
}
|
||||||
query.TimeRange = timeRange
|
query.TimeRange = timeRange
|
||||||
models, err := service.parseQuery([]backend.DataQuery{query})
|
models, err := service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Second*6, models[0].Step)
|
require.Equal(t, time.Second*6, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -110,7 +110,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
To: now.Add(12 * time.Hour),
|
To: now.Add(12 * time.Hour),
|
||||||
}
|
}
|
||||||
query.TimeRange = timeRange
|
query.TimeRange = timeRange
|
||||||
models, err := service.parseQuery([]backend.DataQuery{query})
|
models, err := service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Second*30, models[0].Step)
|
require.Equal(t, time.Second*30, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -128,7 +128,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
To: now.Add(12 * time.Hour),
|
To: now.Add(12 * time.Hour),
|
||||||
}
|
}
|
||||||
query.TimeRange = timeRange
|
query.TimeRange = timeRange
|
||||||
models, err := service.parseQuery([]backend.DataQuery{query})
|
models, err := service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Second*5, models[0].Step)
|
require.Equal(t, time.Second*5, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -140,7 +140,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
"intervalFactor": 1,
|
"intervalFactor": 1,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
}`)
|
}`)
|
||||||
models, err := service.parseQuery([]backend.DataQuery{query})
|
models, err := service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Minute*2, models[0].Step)
|
require.Equal(t, time.Minute*2, models[0].Step)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
To: now.Add(1 * time.Hour),
|
To: now.Add(1 * time.Hour),
|
||||||
}
|
}
|
||||||
query.TimeRange = timeRange
|
query.TimeRange = timeRange
|
||||||
models, err = service.parseQuery([]backend.DataQuery{query})
|
models, err = service.parseQuery([]backend.DataQuery{query}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Second*15, models[0].Step)
|
require.Equal(t, time.Second*15, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -160,7 +160,7 @@ func TestPrometheus(t *testing.T) {
|
|||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"intervalFactor": 10,
|
"intervalFactor": 10,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
}`)})
|
}`)}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Minute*20, models[0].Step)
|
require.Equal(t, time.Minute*20, models[0].Step)
|
||||||
})
|
})
|
||||||
@@ -171,10 +171,23 @@ func TestPrometheus(t *testing.T) {
|
|||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"intervalFactor": 1,
|
"intervalFactor": 1,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
}`)})
|
}`)}, &DatasourceInfo{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, time.Minute*2, models[0].Step)
|
require.Equal(t, time.Minute*2, models[0].Step)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("parsing query model specified scrape-interval in the data source", func(t *testing.T) {
|
||||||
|
models, err := service.parseQuery([]backend.DataQuery{queryContext(`{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}`)}, &DatasourceInfo{
|
||||||
|
TimeInterval: "240s",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, time.Minute*4, models[0].Step)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryContext(json string) backend.DataQuery {
|
func queryContext(json string) backend.DataQuery {
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ describe('PasswordField', () => {
|
|||||||
placeholder: 'enter password',
|
placeholder: 'enter password',
|
||||||
'data-testid': 'password-field',
|
'data-testid': 'password-field',
|
||||||
};
|
};
|
||||||
it('should renders correctly', () => {
|
it('should render correctly', () => {
|
||||||
render(<PasswordField {...props} />);
|
render(<PasswordField {...props} />);
|
||||||
expect(screen.getByTestId('password-field')).toBeInTheDocument();
|
expect(screen.getByTestId('password-field')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
expect(screen.getByRole('switch', { name: 'Show password' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('should able to show password value if clicked on password-reveal icon', () => {
|
it('should able to show password value if clicked on password-reveal icon', () => {
|
||||||
render(<PasswordField {...props} />);
|
render(<PasswordField {...props} />);
|
||||||
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'password');
|
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'password');
|
||||||
fireEvent.click(screen.getByRole('button'));
|
fireEvent.click(screen.getByRole('switch', { name: 'Show password' }));
|
||||||
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'text');
|
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'text');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const PasswordField: FC<Props> = React.forwardRef<HTMLInputElement, Props
|
|||||||
name={showPassword ? 'eye-slash' : 'eye'}
|
name={showPassword ? 'eye-slash' : 'eye'}
|
||||||
type="button"
|
type="button"
|
||||||
aria-controls={id}
|
aria-controls={id}
|
||||||
aria-role="switch"
|
role="switch"
|
||||||
aria-checked={showPassword}
|
aria-checked={showPassword}
|
||||||
aria-label="Show password"
|
aria-label="Show password"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
+8
-6
@@ -1,5 +1,5 @@
|
|||||||
import { toDataFrame, ArrayVector, DataFrame, FieldType, toDataFrameDTO, DataFrameDTO } from '@grafana/data';
|
import { toDataFrame, ArrayVector, DataFrame, FieldType, toDataFrameDTO, DataFrameDTO } from '@grafana/data';
|
||||||
import { prepareTimeSeries, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries';
|
import { prepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries';
|
||||||
|
|
||||||
describe('Prepair time series transformer', () => {
|
describe('Prepair time series transformer', () => {
|
||||||
it('should transform wide to many', () => {
|
it('should transform wide to many', () => {
|
||||||
@@ -19,7 +19,7 @@ describe('Prepair time series transformer', () => {
|
|||||||
format: timeSeriesFormat.TimeSeriesMany,
|
format: timeSeriesFormat.TimeSeriesMany,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(prepareTimeSeries(source, config)).toEqual([
|
expect(prepareTimeSeriesTransformer.transformer(config)(source)).toEqual([
|
||||||
toEquableDataFrame({
|
toEquableDataFrame({
|
||||||
name: 'wide',
|
name: 'wide',
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@@ -59,7 +59,7 @@ describe('Prepair time series transformer', () => {
|
|||||||
format: timeSeriesFormat.TimeSeriesMany,
|
format: timeSeriesFormat.TimeSeriesMany,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(prepareTimeSeries(source, config)).toEqual([
|
expect(prepareTimeSeriesTransformer.transformer(config)(source)).toEqual([
|
||||||
toEquableDataFrame({
|
toEquableDataFrame({
|
||||||
name: 'wide',
|
name: 'wide',
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@@ -107,7 +107,7 @@ describe('Prepair time series transformer', () => {
|
|||||||
format: timeSeriesFormat.TimeSeriesMany,
|
format: timeSeriesFormat.TimeSeriesMany,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(prepareTimeSeries(source, config)).toEqual([
|
expect(prepareTimeSeriesTransformer.transformer(config)(source)).toEqual([
|
||||||
toEquableDataFrame({
|
toEquableDataFrame({
|
||||||
name: 'wide',
|
name: 'wide',
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@@ -162,7 +162,9 @@ describe('Prepair time series transformer', () => {
|
|||||||
format: timeSeriesFormat.TimeSeriesMany,
|
format: timeSeriesFormat.TimeSeriesMany,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(toEquableDataFrames(prepareTimeSeries(source, config))).toEqual(toEquableDataFrames(source));
|
expect(toEquableDataFrames(prepareTimeSeriesTransformer.transformer(config)(source))).toEqual(
|
||||||
|
toEquableDataFrames(source)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty array when no timeseries exist', () => {
|
it('should return empty array when no timeseries exist', () => {
|
||||||
@@ -191,7 +193,7 @@ describe('Prepair time series transformer', () => {
|
|||||||
format: timeSeriesFormat.TimeSeriesMany,
|
format: timeSeriesFormat.TimeSeriesMany,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(prepareTimeSeries(source, config)).toEqual([]);
|
expect(prepareTimeSeriesTransformer.transformer(config)(source)).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
DataTransformerInfo,
|
SynchronousDataTransformerInfo,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
FieldType,
|
FieldType,
|
||||||
DataTransformerID,
|
DataTransformerID,
|
||||||
@@ -63,31 +63,30 @@ export function toTimeSeriesMany(data: DataFrame[]): DataFrame[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareTimeSeries(data: DataFrame[], options: PrepareTimeSeriesOptions): DataFrame[] {
|
export const prepareTimeSeriesTransformer: SynchronousDataTransformerInfo<PrepareTimeSeriesOptions> = {
|
||||||
const format = options?.format ?? timeSeriesFormat.TimeSeriesWide;
|
|
||||||
if (format === timeSeriesFormat.TimeSeriesMany) {
|
|
||||||
return toTimeSeriesMany(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join by the first frame
|
|
||||||
const frame = outerJoinDataFrames({
|
|
||||||
frames: data,
|
|
||||||
joinBy: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
||||||
enforceSort: true,
|
|
||||||
keepOriginIndices: true,
|
|
||||||
});
|
|
||||||
return frame ? [frame] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const prepareTimeSeriesTransformer: DataTransformerInfo<PrepareTimeSeriesOptions> = {
|
|
||||||
id: DataTransformerID.prepareTimeSeries,
|
id: DataTransformerID.prepareTimeSeries,
|
||||||
name: 'Prepare time series',
|
name: 'Prepare time series',
|
||||||
description: `Will stretch data frames from the wide format into the long format. This is really helpful to be able to keep backwards compatability for panels not supporting the new wide format.`,
|
description: `Will stretch data frames from the wide format into the long format. This is really helpful to be able to keep backwards compatability for panels not supporting the new wide format.`,
|
||||||
defaultOptions: {},
|
defaultOptions: {},
|
||||||
|
|
||||||
/**
|
operator: (options) => (source) =>
|
||||||
* Return a modified copy of the series. If the transform is not or should not
|
source.pipe(map((data) => prepareTimeSeriesTransformer.transformer(options)(data))),
|
||||||
* be applied, just return the input series
|
|
||||||
*/
|
transformer: (options: PrepareTimeSeriesOptions) => {
|
||||||
operator: (options) => (source) => source.pipe(map((data) => prepareTimeSeries(data, options))),
|
const format = options?.format ?? timeSeriesFormat.TimeSeriesWide;
|
||||||
|
if (format === timeSeriesFormat.TimeSeriesMany) {
|
||||||
|
return toTimeSeriesMany;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (data: DataFrame[]) => {
|
||||||
|
// Join by the first frame
|
||||||
|
const frame = outerJoinDataFrames({
|
||||||
|
frames: data,
|
||||||
|
joinBy: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||||
|
enforceSort: true,
|
||||||
|
keepOriginIndices: true,
|
||||||
|
});
|
||||||
|
return frame ? [frame] : [];
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { render, waitFor } from '@testing-library/react';
|
|||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { RuleList } from './RuleList';
|
import { RuleList } from './RuleList';
|
||||||
import { byTestId, byText } from 'testing-library-selector';
|
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||||
import { getAllDataSources } from './utils/config';
|
import { getAllDataSources } from './utils/config';
|
||||||
import { fetchRules } from './api/prometheus';
|
import { fetchRules } from './api/prometheus';
|
||||||
@@ -77,6 +77,7 @@ const ui = {
|
|||||||
ruleRow: byTestId('row'),
|
ruleRow: byTestId('row'),
|
||||||
expandedContent: byTestId('expanded-content'),
|
expandedContent: byTestId('expanded-content'),
|
||||||
rulesFilterInput: byTestId('search-query-input'),
|
rulesFilterInput: byTestId('search-query-input'),
|
||||||
|
moreErrorsButton: byRole('button', { name: /more errors/ }),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('RuleList', () => {
|
describe('RuleList', () => {
|
||||||
@@ -164,6 +165,10 @@ describe('RuleList', () => {
|
|||||||
|
|
||||||
const errors = await ui.cloudRulesSourceErrors.find();
|
const errors = await ui.cloudRulesSourceErrors.find();
|
||||||
|
|
||||||
|
expect(errors).not.toHaveTextContent(
|
||||||
|
'Failed to load rules state from Prometheus-broken: this datasource is broken'
|
||||||
|
);
|
||||||
|
userEvent.click(ui.moreErrorsButton.get());
|
||||||
expect(errors).toHaveTextContent('Failed to load rules state from Prometheus-broken: this datasource is broken');
|
expect(errors).toHaveTextContent('Failed to load rules state from Prometheus-broken: this datasource is broken');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user