Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
4d96a216
Commit
4d96a216
authored
Mar 23, 2020
by
Olena Horal-Koretska
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Indicate whether the alert is firing
parent
360c79fb
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
337 additions
and
71 deletions
+337
-71
app/assets/stylesheets/pages/prometheus.scss
app/assets/stylesheets/pages/prometheus.scss
+5
-0
ee/app/assets/javascripts/monitoring/components/alert_widget.vue
...assets/javascripts/monitoring/components/alert_widget.vue
+100
-21
ee/app/assets/javascripts/monitoring/components/alert_widget_form.vue
...s/javascripts/monitoring/components/alert_widget_form.vue
+1
-6
ee/app/assets/javascripts/monitoring/constants.js
ee/app/assets/javascripts/monitoring/constants.js
+6
-0
ee/changelogs/unreleased/208496_Firing_alerts.yml
ee/changelogs/unreleased/208496_Firing_alerts.yml
+5
-0
ee/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
...ontend/monitoring/__snapshots__/alert_widget_spec.js.snap
+28
-5
ee/spec/frontend/monitoring/alert_widget_spec.js
ee/spec/frontend/monitoring/alert_widget_spec.js
+183
-39
locale/gitlab.pot
locale/gitlab.pot
+9
-0
No files found.
app/assets/stylesheets/pages/prometheus.scss
View file @
4d96a216
...
...
@@ -93,6 +93,11 @@
.alert-current-setting
{
max-width
:
240px
;
.badge.badge-danger
{
color
:
$red-500
;
background-color
:
$red-100
;
}
}
.prometheus-graph-cursor
{
...
...
ee/app/assets/javascripts/monitoring/components/alert_widget.vue
View file @
4d96a216
<
script
>
import
{
GlBadge
,
GlLoadingIcon
,
GlModalDirective
}
from
'
@gitlab/ui
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
GlBadge
,
GlLoadingIcon
,
GlModalDirective
,
GlIcon
,
GlTooltip
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
createFlash
from
'
~/flash
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
AlertWidgetForm
from
'
./alert_widget_form.vue
'
;
import
AlertsService
from
'
../services/alerts_service
'
;
import
{
alertsValidator
,
queriesValidator
}
from
'
../validators
'
;
import
{
OPERATORS
}
from
'
../constants
'
;
import
{
values
,
get
}
from
'
lodash
'
;
export
default
{
components
:
{
AlertWidgetForm
,
GlBadge
,
GlLoadingIcon
,
Icon
,
GlIcon
,
GlTooltip
,
GlSprintf
,
},
directives
:
{
GlModal
:
GlModalDirective
,
...
...
@@ -50,16 +53,48 @@ export default {
apiAction
:
'
create
'
,
};
},
i18n
:
{
alertsCountMsg
:
s__
(
'
PrometheusAlerts|%{count} alerts applied
'
),
singleFiringMsg
:
s__
(
'
PrometheusAlerts|Firing: %{alert}
'
),
multipleFiringMsg
:
s__
(
'
PrometheusAlerts|%{firingCount} firing
'
),
firingAlertsTooltip
:
s__
(
'
PrometheusAlerts|Firing: %{alerts}
'
),
},
computed
:
{
alertSummary
()
{
singleAlertSummary
()
{
return
{
message
:
this
.
isFiring
?
this
.
$options
.
i18n
.
singleFiringMsg
:
this
.
thresholds
[
0
],
alert
:
this
.
thresholds
[
0
],
};
},
multipleAlertsSummary
()
{
return
{
message
:
this
.
isFiring
?
`
${
this
.
$options
.
i18n
.
alertsCountMsg
}
,
${
this
.
$options
.
i18n
.
multipleFiringMsg
}
`
:
this
.
$options
.
i18n
.
alertsCountMsg
,
count
:
this
.
thresholds
.
length
,
firingCount
:
this
.
firingAlerts
.
length
,
};
},
thresholds
()
{
const
alertsToManage
=
Object
.
keys
(
this
.
alertsToManage
);
const
alertCountMsg
=
sprintf
(
s__
(
'
PrometheusAlerts|%{count} alerts applied
'
),
{
count
:
alertsToManage
.
length
,
});
return
alertsToManage
.
length
>
1
?
alertCountMsg
:
alertsToManage
.
map
(
this
.
formatAlertSummary
)[
0
];
return
alertsToManage
.
map
(
this
.
formatAlertSummary
);
},
hasAlerts
()
{
return
Boolean
(
Object
.
keys
(
this
.
alertsToManage
).
length
);
},
hasMultipleAlerts
()
{
return
this
.
thresholds
.
length
>
1
;
},
isFiring
()
{
return
Boolean
(
this
.
firingAlerts
.
length
);
},
firingAlerts
()
{
return
values
(
this
.
alertsToManage
).
filter
(
alert
=>
this
.
passedAlertThreshold
(
this
.
getQueryData
(
alert
),
alert
),
);
},
formattedFiringAlerts
()
{
return
this
.
firingAlerts
.
map
(
alert
=>
this
.
formatAlertSummary
(
alert
.
alert_path
));
},
},
created
()
{
...
...
@@ -99,6 +134,25 @@ export default {
return
`
${
alertQuery
.
label
}
${
alert
.
operator
}
${
alert
.
threshold
}
`
;
},
passedAlertThreshold
(
data
,
alert
)
{
const
{
threshold
,
operator
}
=
alert
;
switch
(
operator
)
{
case
OPERATORS
.
greaterThan
:
return
data
.
some
(
value
=>
value
>
threshold
);
case
OPERATORS
.
lessThan
:
return
data
.
some
(
value
=>
value
<
threshold
);
case
OPERATORS
.
equalTo
:
return
data
.
some
(
value
=>
value
===
threshold
);
default
:
return
false
;
}
},
getQueryData
(
alert
)
{
const
alertQuery
=
this
.
relevantQueries
.
find
(
query
=>
query
.
metricId
===
alert
.
metricId
);
return
get
(
alertQuery
,
'
result[0].values
'
,
[]).
map
(
value
=>
get
(
value
,
'
[1]
'
,
null
));
},
showModal
()
{
this
.
$root
.
$emit
(
'
bv::show::modal
'
,
this
.
modalId
);
},
...
...
@@ -159,24 +213,49 @@ export default {
<
template
>
<div
class=
"prometheus-alert-widget dropdown flex-grow-2 overflow-hidden"
>
<span
v-if=
"errorMessage"
ref=
"alertErrorMessage"
class=
"alert-error-message"
>
{{
<gl-loading-icon
v-if=
"isLoading"
:inline=
"true"
/>
<span
v-else-if=
"errorMessage"
ref=
"alertErrorMessage"
class=
"alert-error-message"
>
{{
errorMessage
}}
</span>
<span
v-else
v-else
-if=
"hasAlerts"
ref=
"alertCurrentSetting"
class=
"alert-current-setting
text-secondary cursor-pointer d-flex align-items-end
"
class=
"alert-current-setting
cursor-pointer d-flex
"
@
click=
"showModal"
>
<gl-badge
v-if=
"alertSummary
"
variant=
"secondary"
class=
"d-flex-center text-
secondary text-
truncate"
:variant=
"isFiring ? 'danger' : 'secondary'
"
pill
class=
"d-flex-center text-truncate"
>
<icon
name=
"warning"
class=
"s18 append-right-4"
:size=
"16"
/>
<span
class=
"text-truncate"
>
{{
alertSummary
}}
</span>
<gl-icon
name=
"warning"
:size=
"16"
class=
"flex-shrink-0"
/>
<span
class=
"text-truncate gl-pl-1"
>
<gl-sprintf
:message=
"
hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
"
>
<template
#alert
>
{{
singleAlertSummary
.
alert
}}
</
template
>
<
template
#count
>
{{
multipleAlertsSummary
.
count
}}
</
template
>
<
template
#firingCount
>
{{
multipleAlertsSummary
.
firingCount
}}
</
template
>
</gl-sprintf>
</span>
</gl-badge>
<gl-loading-icon
v-show=
"isLoading"
:inline=
"true"
/>
<gl-tooltip
v-if=
"hasMultipleAlerts && isFiring"
:target=
"() => $refs.alertCurrentSetting"
>
<gl-sprintf
:message=
"$options.i18n.firingAlertsTooltip"
>
<
template
#alerts
>
<div
v-for=
"alert in formattedFiringAlerts"
:key=
"alert.alert_path"
>
{{
alert
}}
</div>
</
template
>
</gl-sprintf>
</gl-tooltip>
</span>
<alert-widget-form
ref=
"widgetForm"
...
...
ee/app/assets/javascripts/monitoring/components/alert_widget_form.vue
View file @
4d96a216
...
...
@@ -18,6 +18,7 @@ import TrackEventDirective from '~/vue_shared/directives/track_event';
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
{
alertsValidator
,
queriesValidator
}
from
'
../validators
'
;
import
{
OPERATORS
}
from
'
../constants
'
;
Vue
.
use
(
Translate
);
...
...
@@ -33,12 +34,6 @@ const SUBMIT_BUTTON_CLASS = {
delete
:
'
btn-remove
'
,
};
const
OPERATORS
=
{
greaterThan
:
'
>
'
,
equalTo
:
'
==
'
,
lessThan
:
'
<
'
,
};
export
default
{
components
:
{
GlButton
,
...
...
ee/app/assets/javascripts/monitoring/constants.js
0 → 100644
View file @
4d96a216
// eslint-disable-next-line import/prefer-default-export
export
const
OPERATORS
=
{
greaterThan
:
'
>
'
,
equalTo
:
'
==
'
,
lessThan
:
'
<
'
,
};
ee/changelogs/unreleased/208496_Firing_alerts.yml
0 → 100644
View file @
4d96a216
---
title
:
Indicate whether the alert is firing
merge_request
:
27825
author
:
type
:
added
ee/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
View file @
4d96a216
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertWidget
displays a warning icon and matches snapshop
t 1`] = `
exports[`AlertWidget
Alert firing displays a warning icon and matches snapsho
t 1`] = `
<gl-badge-stub
class="d-flex-center text-secondary text-truncate"
class="d-flex-center text-truncate"
pill=""
variant="danger"
>
<gl-icon-stub
class="flex-shrink-0"
name="warning"
size="16"
/>
<span
class="text-truncate gl-pl-1"
>
Firing:
alert-label > 42
</span>
</gl-badge-stub>
`;
exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
<gl-badge-stub
class="d-flex-center text-truncate"
pill=""
variant="secondary"
>
<icon-stub
class="
s18 append-right-4
"
<
gl-
icon-stub
class="
flex-shrink-0
"
name="warning"
size="16"
/>
<span
class="text-truncate"
class="text-truncate
gl-pl-1
"
>
alert-label > 42
</span>
...
...
ee/spec/frontend/monitoring/alert_widget_spec.js
View file @
4d96a216
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlLoadingIcon
,
GlBadge
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
,
Gl
Tooltip
,
GlSprintf
,
Gl
Badge
}
from
'
@gitlab/ui
'
;
import
AlertWidget
from
'
ee/monitoring/components/alert_widget.vue
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
...
...
@@ -26,9 +26,36 @@ jest.mock(
describe
(
'
AlertWidget
'
,
()
=>
{
let
wrapper
;
const
nonFiringAlertResult
=
[
{
values
:
[[
0
,
1
],
[
1
,
42
],
[
2
,
41
]],
},
];
const
firingAlertResult
=
[
{
values
:
[[
0
,
42
],
[
1
,
43
],
[
2
,
44
]],
},
];
const
metricId
=
'
5
'
;
const
alertPath
=
'
my/alert.json
'
;
const
relevantQueries
=
[{
metricId
,
label
:
'
alert-label
'
,
alert_path
:
alertPath
}];
const
relevantQueries
=
[
{
metricId
,
label
:
'
alert-label
'
,
alert_path
:
alertPath
,
result
:
nonFiringAlertResult
,
},
];
const
firingRelevantQueries
=
[
{
metricId
,
label
:
'
alert-label
'
,
alert_path
:
alertPath
,
result
:
firingAlertResult
,
},
];
const
defaultProps
=
{
alertsEndpoint
:
''
,
...
...
@@ -50,16 +77,23 @@ describe('AlertWidget', () => {
const
createComponent
=
propsData
=>
{
wrapper
=
shallowMount
(
AlertWidget
,
{
stubs
:
{
GlTooltip
,
GlSprintf
},
propsData
:
{
...
defaultProps
,
...
propsData
,
},
});
};
const
hasLoadingIcon
=
()
=>
wrapper
.
contains
(
GlLoadingIcon
);
const
findWidgetForm
=
()
=>
wrapper
.
find
({
ref
:
'
widgetForm
'
});
const
findAlertErrorMessage
=
()
=>
wrapper
.
find
({
ref
:
'
alertErrorMessage
'
});
const
findCurrentSettings
=
()
=>
wrapper
.
find
({
ref
:
'
alertCurrentSetting
'
});
const
findCurrentSettingsText
=
()
=>
wrapper
.
find
({
ref
:
'
alertCurrentSetting
'
})
.
text
()
.
replace
(
/
\s\s
+/g
,
'
'
);
const
findBadge
=
()
=>
wrapper
.
find
(
GlBadge
);
const
findTooltip
=
()
=>
wrapper
.
find
(
GlTooltip
);
afterEach
(()
=>
{
wrapper
.
destroy
();
...
...
@@ -77,14 +111,14 @@ describe('AlertWidget', () => {
return
wrapper
.
vm
.
$nextTick
()
.
then
(()
=>
{
expect
(
wrapper
.
find
(
GlLoadingIcon
).
isVisible
()).
toBe
(
true
);
expect
(
hasLoadingIcon
()).
toBe
(
true
);
expect
(
findWidgetForm
().
props
(
'
disabled
'
)).
toBe
(
true
);
resolveReadAlert
({
operator
:
'
==
'
,
threshold
:
42
});
})
.
then
(()
=>
waitForPromises
())
.
then
(()
=>
{
expect
(
wrapper
.
find
(
GlLoadingIcon
).
isVisible
()).
toBe
(
false
);
expect
(
hasLoadingIcon
()).
toBe
(
false
);
expect
(
findWidgetForm
().
props
(
'
disabled
'
)).
toBe
(
false
);
});
});
...
...
@@ -92,53 +126,163 @@ describe('AlertWidget', () => {
it
(
'
displays an error message when fetch fails
'
,
()
=>
{
mockReadAlert
.
mockRejectedValue
();
createComponent
(
propsWithAlert
);
expect
(
wrapper
.
find
(
GlLoadingIcon
).
isVisible
()).
toBe
(
true
);
expect
(
hasLoadingIcon
()).
toBe
(
true
);
return
waitForPromises
().
then
(()
=>
{
expect
(
createFlash
).
toHaveBeenCalled
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
isVisible
()).
toBe
(
false
);
expect
(
hasLoadingIcon
()).
toBe
(
false
);
});
});
it
(
'
displays an alert summary when there is a single alert
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
createComponent
(
propsWithAlertData
);
describe
(
'
Alert not firing
'
,
()
=>
{
it
(
'
displays a warning icon and matches snapshot
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
createComponent
(
propsWithAlertData
);
expect
(
wrapper
.
text
()).
toContain
(
'
alert-label > 42
'
);
});
return
waitForPromises
().
then
(()
=>
{
expect
(
findBadge
().
element
).
toMatchSnapshot
();
});
});
it
(
'
displays a warning icon and matches snapshopt
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
createComponent
(
propsWithAlertData
);
it
(
'
displays an alert summary when there is a single alert
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
createComponent
(
propsWithAlertData
);
return
waitForPromises
().
then
(()
=>
{
expect
(
findCurrentSettingsText
()).
toEqual
(
'
alert-label > 42
'
);
});
});
expect
(
findBadge
().
element
).
toMatchSnapshot
();
it
(
'
displays a combined alert summary when there are multiple alerts
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
const
propsWithManyAlerts
=
{
relevantQueries
:
[
...
relevantQueries
,
...[
{
metricId
:
'
6
'
,
alert_path
:
'
my/alert2.json
'
,
label
:
'
alert-label2
'
,
result
:
[{
values
:
[]
}],
},
],
],
alertsToManage
:
{
'
my/alert.json
'
:
{
operator
:
'
>
'
,
threshold
:
42
,
alert_path
:
alertPath
,
metricId
,
},
'
my/alert2.json
'
:
{
operator
:
'
==
'
,
threshold
:
900
,
alert_path
:
'
my/alert2.json
'
,
metricId
:
'
6
'
,
},
},
};
createComponent
(
propsWithManyAlerts
);
return
waitForPromises
().
then
(()
=>
{
expect
(
findCurrentSettingsText
()).
toContain
(
'
2 alerts applied
'
);
});
});
});
it
(
'
displays a combined alert summary when there are multiple alerts
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
const
propsWithManyAlerts
=
{
relevantQueries
:
relevantQueries
.
concat
([
{
metricId
:
'
6
'
,
alert_path
:
'
my/alert2.json
'
,
label
:
'
alert-label2
'
},
]),
alertsToManage
:
{
'
my/alert.json
'
:
{
operator
:
'
>
'
,
threshold
:
42
,
alert_path
:
alertPath
,
metricId
,
},
'
my/alert2.json
'
:
{
operator
:
'
==
'
,
threshold
:
900
,
alert_path
:
'
my/alert2.json
'
,
metricId
:
'
6
'
,
describe
(
'
Alert firing
'
,
()
=>
{
it
(
'
displays a warning icon and matches snapshot
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
propsWithAlertData
.
relevantQueries
=
firingRelevantQueries
;
createComponent
(
propsWithAlertData
);
return
waitForPromises
().
then
(()
=>
{
expect
(
findBadge
().
element
).
toMatchSnapshot
();
});
});
it
(
'
displays an alert summary when there is a single alert
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
propsWithAlertData
.
relevantQueries
=
firingRelevantQueries
;
createComponent
(
propsWithAlertData
);
return
waitForPromises
().
then
(()
=>
{
expect
(
findCurrentSettingsText
()).
toEqual
(
'
Firing: alert-label > 42
'
);
});
});
it
(
'
displays a combined alert summary when there are multiple alerts
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
const
propsWithManyAlerts
=
{
relevantQueries
:
[
...
firingRelevantQueries
,
...[
{
metricId
:
'
6
'
,
alert_path
:
'
my/alert2.json
'
,
label
:
'
alert-label2
'
,
result
:
[{
values
:
[]
}],
},
],
],
alertsToManage
:
{
'
my/alert.json
'
:
{
operator
:
'
>
'
,
threshold
:
42
,
alert_path
:
alertPath
,
metricId
,
},
'
my/alert2.json
'
:
{
operator
:
'
==
'
,
threshold
:
900
,
alert_path
:
'
my/alert2.json
'
,
metricId
:
'
6
'
,
},
},
},
};
createComponent
(
propsWithManyAlerts
);
};
createComponent
(
propsWithManyAlerts
);
return
waitForPromises
().
then
(()
=>
{
expect
(
findCurrentSettingsText
()).
toContain
(
'
2 alerts applied, 1 firing
'
);
});
});
expect
(
findCurrentSettings
().
text
()).
toEqual
(
'
2 alerts applied
'
);
it
(
'
should display tooltip with thresholds summary
'
,
()
=>
{
mockReadAlert
.
mockResolvedValue
({
operator
:
'
>
'
,
threshold
:
42
});
const
propsWithManyAlerts
=
{
relevantQueries
:
[
...
firingRelevantQueries
,
...[
{
metricId
:
'
6
'
,
alert_path
:
'
my/alert2.json
'
,
label
:
'
alert-label2
'
,
result
:
[{
values
:
[]
}],
},
],
],
alertsToManage
:
{
'
my/alert.json
'
:
{
operator
:
'
>
'
,
threshold
:
42
,
alert_path
:
alertPath
,
metricId
,
},
'
my/alert2.json
'
:
{
operator
:
'
==
'
,
threshold
:
900
,
alert_path
:
'
my/alert2.json
'
,
metricId
:
'
6
'
,
},
},
};
createComponent
(
propsWithManyAlerts
);
return
waitForPromises
().
then
(()
=>
{
expect
(
findTooltip
()
.
text
()
.
replace
(
/
\s\s
+/g
,
'
'
),
).
toEqual
(
'
Firing: alert-label > 42
'
);
});
});
});
it
(
'
creates an alert with an appropriate handler
'
,
()
=>
{
...
...
locale/gitlab.pot
View file @
4d96a216
...
...
@@ -15900,6 +15900,9 @@ msgstr ""
msgid "PrometheusAlerts|%{count} alerts applied"
msgstr ""
msgid "PrometheusAlerts|%{firingCount} firing"
msgstr ""
msgid "PrometheusAlerts|Add alert"
msgstr ""
...
...
@@ -15918,6 +15921,12 @@ msgstr ""
msgid "PrometheusAlerts|Error saving alert"
msgstr ""
msgid "PrometheusAlerts|Firing: %{alerts}"
msgstr ""
msgid "PrometheusAlerts|Firing: %{alert}"
msgstr ""
msgid "PrometheusAlerts|Operator"
msgstr ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment