Merge pull request #177 from cloudflare/hide-annotations

Allow hiding annotations in the UI
This commit is contained in:
Łukasz Mierzwa
2017-09-07 15:21:55 -04:00
committed by GitHub
21 changed files with 406 additions and 287 deletions

View File

@@ -191,6 +191,62 @@ This option can also be set using `-alertmanager.uris` flag. Example:
This variable is required and there is no default value.
#### ANNOTATIONS_DEFAULT_HIDDEN
Enabling this option will hide all annotations in the UI, except for those
that are listed in the `ANNOTATIONS_VISIBLE` option.
Examples:
ANNOTATIONS_DEFAULT_HIDDEN=true
ANNOTATIONS_DEFAULT_HIDDEN=false
This option can also be set using `-annotations.default.hidden` flag. Example:
$ unsee -annotations.default.hidden
Default is `false`, which means that all annotations are visible.
#### ANNOTATIONS_HIDDEN
List of annotation names that should be hidden in the UI. Hidden annotations
can still be accessed if needed by clicking on a zoom button that will appear
if there are any hidden annotations.
Examples:
ANNOTATIONS_HIDDEN=summary
ANNOTATIONS_HIDDEN="summary owner"
This option can also be set using `-annotations.hidden` flag. Example:
$ unsee -annotations.hidden "summary owner"
This variable is optional and default is not set (all annotations are visible),
unless user enables `ANNOTATIONS_DEFAULT_HIDDEN` option.
#### ANNOTATIONS_VISIBLE
List of annotation names that should be visible in the UI. This option is only
useful when `ANNOTATIONS_DEFAULT_HIDDEN` is set.
With `ANNOTATIONS_DEFAULT_HIDDEN` all annotations are hidden by default unless
they are present in the `ANNOTATIONS_VISIBLE` option.
If `ANNOTATIONS_DEFAULT_HIDDEN` is not enabled this option is no-op.
Examples:
ANNOTATIONS_VISIBLE=summary
ANNOTATIONS_VISIBLE="summary owner"
This option can also be set using `-annotations.visible` flag. Example:
$ unsee -annotations.visible "summary owner"
This variable is optional and default is not set.
If `ANNOTATIONS_HIDDEN` is enabled then all annotations are hidden by default.
If `ANNOTATIONS_HIDDEN` is not enabled then all annotations are visible by
default.
#### DEBUG
Will enable [gin](https://github.com/gin-gonic/gin) debug mode. This will

View File

@@ -29,8 +29,9 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"alert": "Memory usage exceeding threshold",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "alert", Value: "Memory usage exceeding threshold"},
models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true},
},
Labels: map[string]string{
"alertname": "Memory_Usage_Too_High",
@@ -48,9 +49,6 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{
"dashboard": "http://localhost/dashboard.html",
},
},
},
id: "099c5ca6d1c92f615b13056b935d0c8dee70f18c",
@@ -69,8 +67,9 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"alert": "Memory usage exceeding threshold",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "alert", Value: "Memory usage exceeding threshold"},
models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -86,9 +85,6 @@ var groupTests = []groupTest{
"instance": "server2",
"job": "node_exporter",
},
Links: map[string]string{
"dashboard": "http://localhost/dashboard.html",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
@@ -109,8 +105,8 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -126,13 +122,12 @@ var groupTests = []groupTest{
"instance": "server3",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -148,13 +143,12 @@ var groupTests = []groupTest{
"instance": "server4",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -170,7 +164,6 @@ var groupTests = []groupTest{
"instance": "server5",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
@@ -191,8 +184,8 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -214,13 +207,12 @@ var groupTests = []groupTest{
"instance": "server6",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -245,13 +237,12 @@ var groupTests = []groupTest{
"instance": "server7",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -272,7 +263,6 @@ var groupTests = []groupTest{
"instance": "server8",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
@@ -292,8 +282,9 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -311,13 +302,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{
"url": "http://localhost/example.html",
},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -335,11 +323,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -357,11 +344,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -379,11 +365,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -401,11 +386,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -428,11 +412,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -459,11 +442,10 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"alertname": "Host_Down",
@@ -486,7 +468,6 @@ var groupTests = []groupTest{
},
},
Receiver: "by-name",
Links: map[string]string{},
},
},
id: "58c6a3467cebc53abe68ecbe8643ce478c5a1573",
@@ -505,8 +486,9 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"alert": "Less than 10% disk space is free",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "alert", Value: "Less than 10% disk space is free"},
models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -522,9 +504,6 @@ var groupTests = []groupTest{
"instance": "server5",
"job": "node_exporter",
},
Links: map[string]string{
"dashboard": "http://localhost/dashboard.html",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
@@ -545,8 +524,9 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -562,15 +542,12 @@ var groupTests = []groupTest{
"instance": "server1",
"job": "node_ping",
},
Links: map[string]string{
"url": "http://localhost/example.html",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -586,7 +563,6 @@ var groupTests = []groupTest{
"instance": "server2",
"job": "node_ping",
},
Links: map[string]string{},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
@@ -606,9 +582,10 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
"help": "Example help annotation",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "help", Value: "Example help annotation"},
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -629,15 +606,12 @@ var groupTests = []groupTest{
"instance": "web1",
"job": "node_exporter",
},
Links: map[string]string{
"url": "http://localhost/example.html",
},
State: models.AlertStateSuppressed,
Receiver: "by-name",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -653,7 +627,6 @@ var groupTests = []groupTest{
"instance": "web2",
"job": "node_exporter",
},
Links: map[string]string{},
State: models.AlertStateActive,
Receiver: "by-name",
},
@@ -673,8 +646,9 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"alert": "Less than 10% disk space is free",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "alert", Value: "Less than 10% disk space is free"},
models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -690,9 +664,6 @@ var groupTests = []groupTest{
"instance": "server5",
"job": "node_exporter",
},
Links: map[string]string{
"dashboard": "http://localhost/dashboard.html",
},
State: models.AlertStateActive,
Receiver: "by-name",
},
@@ -713,9 +684,10 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: map[string]string{
"help": "Example help annotation",
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "help", Value: "Example help annotation"},
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -736,15 +708,12 @@ var groupTests = []groupTest{
"instance": "web1",
"job": "node_exporter",
},
Links: map[string]string{
"url": "http://localhost/example.html",
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: map[string]string{
"summary": "Example summary",
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
@@ -760,7 +729,6 @@ var groupTests = []groupTest{
"instance": "web2",
"job": "node_exporter",
},
Links: map[string]string{},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
@@ -875,10 +843,6 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler
t.Errorf("[%s] Labels mismatch on alert receiver='%s', expected labels=%v but got %v",
version, expectedAlert.Receiver, expectedAlert.Labels, gotAlert.Labels)
}
if !reflect.DeepEqual(gotAlert.Links, expectedAlert.Links) {
t.Errorf("[%s] Links mismatch on alert receiver='%s' labels=%v expected %v but got %v",
version, expectedAlert.Receiver, expectedAlert.Labels, expectedAlert.Links, gotAlert.Links)
}
if len(gotAlert.Alertmanager) != len(expectedAlert.Alertmanager) {
t.Errorf("[%s] Expected %d alertmanager instances but got %d on alert receiver='%s' labels=%v",
version, len(expectedAlert.Alertmanager), len(gotAlert.Alertmanager), gotAlert.Receiver, expectedAlert.Labels)

View File

@@ -33,6 +33,7 @@ AlertGroup.prototype.Added = function() {
var elem = $("#" + this.id);
ui.setupGroupTooltips(elem);
ui.setupGroupLinkHover(elem);
ui.setupGroupAnnotationToggles(elem);
};
AlertGroup.prototype.Update = function() {

View File

@@ -36,8 +36,9 @@ var templates = {},
silenceFormFatal: "#silence-form-fatal",
silenceFormLoading: "#silence-form-loading",
// label button
// alert partials
buttonLabel: "#label-button-filter",
alertAnnotation: "#alert-annotation",
// alert group
alertGroup: "#alert-group",

View File

@@ -5,6 +5,7 @@ const $ = require("jquery");
const alerts = require("./alerts");
const autocomplete = require("./autocomplete");
const filters = require("./filters");
const grid = require("./grid");
const summary = require("./summary");
const templates = require("./templates");
const unsee = require("./unsee");
@@ -75,6 +76,29 @@ function setupGroupTooltips(groupElem) {
});
}
function setupGroupAnnotationToggles(groupElem) {
$(groupElem).on("click", "[data-toggle=toggle-hidden-annotation]", function() {
var alert = $(this).parent();
var icon = $(this).find("i.fa");
var showingHidden = icon.hasClass("fa-search-minus");
if (showingHidden) {
// we're currently showing hidden annotations, so the action is to hide them
icon.removeClass("fa-search-minus").addClass("fa-search-plus");
$.each(alert.find(".hidden-annotation"), function(i, annotation){
$(annotation).addClass("hidden");
});
} else {
// we're currently hiding hidden annotations, so the action is to show them
icon.removeClass("fa-search-plus").addClass("fa-search-minus");
$.each(alert.find(".hidden-annotation"), function(i, annotation){
$(annotation).removeClass("hidden");
});
}
grid.redraw();
});
}
exports.setupModal = setupModal;
exports.setupGroupTooltips = setupGroupTooltips;
exports.setupGroupLinkHover = setupGroupLinkHover;
exports.setupGroupAnnotationToggles = setupGroupAnnotationToggles;

View File

@@ -27,28 +27,32 @@
</script>
<script type="application/json" id="alert-group-annotations">
<% _.each(sortMapByKey(alert.annotations), function(annotation) { %>
<div class="well well-sm annotation-well">
<i class="fa fa-info-circle text-muted" title="<%- annotation.key %>" data-toggle="tooltip" data-placement="top"/>
<% if (annotation.value) { %>
<%= linkify(_.escape(annotation.value)) %>
<% } else { %>
<span class="text-muted">
[ missing annotation value ]
</span>
<% } %>
</div>
<% var hiddenCount = 0 %>
<% _.each(alert.annotations, function(annotation) { %>
<% if (annotation.isLink === false) { %>
<% if (annotation.visible === false) { hiddenCount++ } %>
<%= renderTemplate('alertAnnotation', {annotation: annotation}) %>
<% } %>
<% }) %>
<% _.each(alert.links, function(url, text) { %>
<a class="label label-list label-default"
href="<%= url %>"
target="_blank"
title="<%= url %>"
data-toggle="tooltip"
data-placement="top">
<i class="fa fa-external-link"/>
<%- text %>
</a>
<% if (hiddenCount) { %>
<span class="label label-default label-list cursor-pointer"
data-toggle="toggle-hidden-annotation"
type="button">
<i class="fa fa-search-plus" title="Toggle hidden annotations" data-toggle="tooltip" data-placement="top" />
</span>
<% } %>
<% _.each(alert.annotations, function(annotation) { %>
<% if (annotation.isLink) { %>
<a class="label label-list label-default"
href="<%= annotation.value %>"
target="_blank"
title="<%= annotation.value %>"
data-toggle="tooltip"
data-placement="top">
<i class="fa fa-external-link"/>
<%- annotation.name %>
</a>
<% } %>
<% }) %>
</script>
@@ -308,3 +312,14 @@
<%- label.text %>
</<%= elem %>>
</script>
<script type="application/json" id="alert-annotation">
<% var cls = "" %>
<% if (annotation.visible === false) { %>
<% cls = "hidden hidden-annotation" %>
<% } %>
<div class="well well-sm annotation-well <%- cls %>">
<i class="fa fa-info-circle text-muted" title="<%- annotation.name %>" data-toggle="tooltip" data-placement="top"/>
<%= linkify(_.escape(annotation.value)) %>
</div>
</script>

View File

@@ -172,8 +172,6 @@ func (am *Alertmanager) pullAlerts(version string) error {
},
}
alert.Annotations, alert.Links = transform.DetectLinks(alert.Annotations)
transform.ColorLabel(colors, "@receiver", alert.Receiver)
for k, v := range alert.Labels {
transform.ColorLabel(colors, k, v)

View File

@@ -24,20 +24,23 @@ func (mvd *spaceSeparatedList) Decode(value string) error {
}
type configEnvs struct {
AlertmanagerTimeout time.Duration `envconfig:"ALERTMANAGER_TIMEOUT" default:"40s" help:"Timeout for all request send to Alertmanager"`
AlertmanagerTTL time.Duration `envconfig:"ALERTMANAGER_TTL" default:"1m" help:"TTL for Alertmanager alerts and silences"`
AlertmanagerURIs spaceSeparatedList `envconfig:"ALERTMANAGER_URIS" required:"true" help:"List of Alertmanager URIs (name:uri)"`
ColorLabelsStatic spaceSeparatedList `envconfig:"COLOR_LABELS_STATIC" help:"List of label names that should have the same (but distinct) color"`
ColorLabelsUnique spaceSeparatedList `envconfig:"COLOR_LABELS_UNIQUE" help:"List of label names that should have unique color"`
Debug bool `envconfig:"DEBUG" default:"false" help:"Enable debug mode"`
FilterDefault string `envconfig:"FILTER_DEFAULT" help:"Default filter string"`
JiraRegexp spaceSeparatedList `envconfig:"JIRA_REGEX" help:"List of JIRA regex rules"`
Port int `envconfig:"PORT" default:"8080" help:"HTTP port to listen on"`
SentryDSN string `envconfig:"SENTRY_DSN" help:"Sentry DSN for Go exceptions"`
SentryPublicDSN string `envconfig:"SENTRY_PUBLIC_DSN" help:"Sentry DSN for javascript exceptions"`
StripLabels spaceSeparatedList `envconfig:"STRIP_LABELS" help:"List of labels to ignore"`
KeepLabels spaceSeparatedList `envconfig:"KEEP_LABELS" help:"List of labels to keep, all other labels will be stripped"`
WebPrefix string `envconfig:"WEB_PREFIX" default:"/" help:"URL prefix"`
AlertmanagerTimeout time.Duration `envconfig:"ALERTMANAGER_TIMEOUT" default:"40s" help:"Timeout for all request send to Alertmanager"`
AlertmanagerTTL time.Duration `envconfig:"ALERTMANAGER_TTL" default:"1m" help:"TTL for Alertmanager alerts and silences"`
AlertmanagerURIs spaceSeparatedList `envconfig:"ALERTMANAGER_URIS" required:"true" help:"List of Alertmanager URIs (name:uri)"`
AnnotationsHidden spaceSeparatedList `envconfig:"ANNOTATIONS_HIDDEN" help:"List of annotations that are hidden by default"`
AnnotationsDefaultHidden bool `envconfig:"ANNOTATIONS_DEFAULT_HIDDEN" default:"false" help:"Hide all annotations by default unless listed in ANNOTATIONS_VISIBLE"`
AnnotationsVisible spaceSeparatedList `envconfig:"ANNOTATIONS_VISIBLE" help:"List of annotations that are visible by default"`
ColorLabelsStatic spaceSeparatedList `envconfig:"COLOR_LABELS_STATIC" help:"List of label names that should have the same (but distinct) color"`
ColorLabelsUnique spaceSeparatedList `envconfig:"COLOR_LABELS_UNIQUE" help:"List of label names that should have unique color"`
Debug bool `envconfig:"DEBUG" default:"false" help:"Enable debug mode"`
FilterDefault string `envconfig:"FILTER_DEFAULT" help:"Default filter string"`
JiraRegexp spaceSeparatedList `envconfig:"JIRA_REGEX" help:"List of JIRA regex rules"`
Port int `envconfig:"PORT" default:"8080" help:"HTTP port to listen on"`
SentryDSN string `envconfig:"SENTRY_DSN" help:"Sentry DSN for Go exceptions"`
SentryPublicDSN string `envconfig:"SENTRY_PUBLIC_DSN" help:"Sentry DSN for javascript exceptions"`
StripLabels spaceSeparatedList `envconfig:"STRIP_LABELS" help:"List of labels to ignore"`
KeepLabels spaceSeparatedList `envconfig:"KEEP_LABELS" help:"List of labels to keep, all other labels will be stripped"`
WebPrefix string `envconfig:"WEB_PREFIX" default:"/" help:"URL prefix"`
}
// Config exposes all options required to run

View File

@@ -27,7 +27,7 @@ func (filter *fuzzyFilter) init(name string, matcher *matcherT, rawText string,
func (filter *fuzzyFilter) Match(alert *models.Alert, matches int) bool {
if filter.IsValid {
for _, val := range alert.Annotations {
if filter.Matcher.Compare(val, filter.Value) {
if filter.Matcher.Compare(val.Value, filter.Value) {
filter.Hits++
return true
}

View File

@@ -355,20 +355,32 @@ var tests = []filterTest{
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.Alert{Annotations: map[string]string{"key": "abc"}},
IsMatch: true,
Alert: models.Alert{
Annotations: models.Annotations{
models.Annotation{Name: "key", Value: "abc"},
},
},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.Alert{Annotations: map[string]string{"key": "ccc abc"}},
IsMatch: true,
Alert: models.Alert{
Annotations: models.Annotations{
models.Annotation{Name: "key", Value: "ccc abc"},
},
},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.Alert{Annotations: map[string]string{"abc": "zzz"}},
IsMatch: false,
Alert: models.Alert{
Annotations: models.Annotations{
models.Annotation{Name: "abc", Value: "zzz"},
},
},
IsMatch: false,
},
filterTest{
Expression: "abc",

View File

@@ -103,7 +103,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler
}
a := models.Alert{
Receiver: rcv.Name,
Annotations: a.Annotations,
Annotations: models.AnnotationsFromMap(a.Annotations),
Labels: a.Labels,
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,

View File

@@ -102,7 +102,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler
}
a := models.Alert{
Receiver: rcv.Name,
Annotations: a.Annotations,
Annotations: models.AnnotationsFromMap(a.Annotations),
Labels: a.Labels,
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,

View File

@@ -101,7 +101,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler
}
a := models.Alert{
Receiver: rcv.Name,
Annotations: a.Annotations,
Annotations: models.AnnotationsFromMap(a.Annotations),
Labels: a.Labels,
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,

View File

@@ -105,7 +105,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler
}
a := models.Alert{
Receiver: rcv.Name,
Annotations: a.Annotations,
Annotations: models.AnnotationsFromMap(a.Annotations),
Labels: a.Labels,
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,

View File

@@ -30,7 +30,7 @@ var AlertStateList = []string{
// it's pulled out of annotation map and returned under links field,
// unsee UI used this to show links differently than other annotations
type Alert struct {
Annotations map[string]string `json:"annotations"`
Annotations Annotations `json:"annotations"`
Labels map[string]string `json:"labels"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
@@ -44,7 +44,6 @@ type Alert struct {
// unsee fields
Alertmanager []AlertmanagerInstance `json:"alertmanager"`
Receiver string `json:"receiver"`
Links map[string]string `json:"links"`
// fingerprints are precomputed for speed
labelsFP string `hash:"-"`
contentFP string `hash:"-"`

View File

@@ -72,9 +72,17 @@ func BenchmarkLabelsFingerprint(b *testing.B) {
func BenchmarkLabelsContent(b *testing.B) {
alert := models.Alert{
Annotations: map[string]string{
"foo": "bar",
"abc": "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...",
Annotations: models.Annotations{
models.Annotation{
Name: "foo",
Value: "bar",
Visible: true,
},
models.Annotation{
Name: "abc",
Value: "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...",
Visible: true,
},
},
Labels: map[string]string{
"foo1": "bar1",

View File

@@ -0,0 +1,84 @@
package models
import (
"net/url"
"sort"
"github.com/cloudflare/unsee/internal/config"
"github.com/cloudflare/unsee/internal/slices"
)
// Annotation extends Alertmanager scheme of key:value with additional data
// to control how given annotation should be rendered
type Annotation struct {
Name string `json:"name"`
Value string `json:"value"`
Visible bool `json:"visible"`
IsLink bool `json:"isLink"`
}
// Annotations is a slice of Annotation structs, needed to implement sorting
type Annotations []Annotation
func (a Annotations) Len() int {
return len(a)
}
func (a Annotations) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a Annotations) Less(i, j int) bool {
return a[i].Name < a[j].Name
}
// AnnotationsFromMap will convert a map[string]string to a list of Annotation
// instances, it takes care of setting proper value for Visible attribute
func AnnotationsFromMap(m map[string]string) Annotations {
annotations := Annotations{}
for name, value := range m {
a := Annotation{
Name: name,
Value: value,
Visible: isVisible(name),
IsLink: isLink(value),
}
annotations = append(annotations, a)
}
sort.Sort(annotations)
return annotations
}
var linkSchemes = []string{
"ftp",
"http",
"https",
}
func isLink(s string) bool {
u, err := url.Parse(s)
if err != nil {
return false
}
if slices.StringInSlice(linkSchemes, u.Scheme) {
// parses with url.Parse and scheme is in the list of supported schemes
return true
}
return false
}
func isVisible(name string) bool {
if slices.StringInSlice(config.Config.AnnotationsVisible, name) {
// annotation was explicitly marked as visible
return true
}
if slices.StringInSlice(config.Config.AnnotationsHidden, name) {
// annotation was explicitly marked as hidden
return false
}
if config.Config.AnnotationsDefaultHidden {
// user specified that default is to hide anything without explicit rules
return false
}
// default to show everything
return true
}

View File

@@ -0,0 +1,85 @@
package models_test
import (
"reflect"
"testing"
"github.com/cloudflare/unsee/internal/models"
)
type annotationMapsTestCase struct {
annotationMap map[string]string
annotations models.Annotations
}
var annotationMapsTestCases = []annotationMapsTestCase{
annotationMapsTestCase{
annotationMap: map[string]string{
"foo": "bar",
},
annotations: models.Annotations{
models.Annotation{
Name: "foo",
Value: "bar",
Visible: true,
IsLink: false,
},
},
},
annotationMapsTestCase{
annotationMap: map[string]string{
"foo": "http://localhost",
},
annotations: models.Annotations{
models.Annotation{
Name: "foo",
Value: "http://localhost",
Visible: true,
IsLink: true,
},
},
},
annotationMapsTestCase{
annotationMap: map[string]string{
"foo": "ftp://localhost",
},
annotations: models.Annotations{
models.Annotation{
Name: "foo",
Value: "ftp://localhost",
Visible: true,
IsLink: true,
},
},
},
annotationMapsTestCase{
annotationMap: map[string]string{
"foo": "https://localhost/xxx",
"abc": "xyz",
},
annotations: models.Annotations{
models.Annotation{
Name: "abc",
Value: "xyz",
Visible: true,
IsLink: false,
},
models.Annotation{
Name: "foo",
Value: "https://localhost/xxx",
Visible: true,
IsLink: true,
},
},
},
}
func TestAnnotationsFromMap(t *testing.T) {
for _, testCase := range annotationMapsTestCases {
result := models.AnnotationsFromMap(testCase.annotationMap)
if !reflect.DeepEqual(testCase.annotations, result) {
t.Errorf("AnnotationsFromMap result mismatch for map %v, expected %v got %v",
testCase.annotationMap, testCase.annotations, result)
}
}
}

View File

@@ -1,35 +0,0 @@
package transform
import (
"net/url"
"github.com/cloudflare/unsee/internal/slices"
)
// list of URI schema which we turn into links in the UI
var schemes = []string{
"ftp",
"http",
"https",
}
// DetectLinks takes alert annotation dict and returns two dicts:
// first with regular annotations
// secondd with annotations where values are URLs
func DetectLinks(sourceAnnotations map[string]string) (map[string]string, map[string]string) {
links := make(map[string]string)
annotations := make(map[string]string)
for k, v := range sourceAnnotations {
u, err := url.Parse(v)
if err != nil {
annotations[k] = v
} else if slices.StringInSlice(schemes, u.Scheme) {
links[k] = v
} else {
annotations[k] = v
}
}
return annotations, links
}

View File

@@ -1,102 +0,0 @@
package transform_test
import (
"reflect"
"testing"
"github.com/cloudflare/unsee/internal/transform"
)
type linkTest struct {
before map[string]string
after map[string]string
links map[string]string
}
var linkTests = []linkTest{
linkTest{
before: map[string]string{},
after: map[string]string{},
links: map[string]string{},
},
linkTest{
before: map[string]string{
"key1": "value 1",
"key2": "value2",
"level": "info",
},
after: map[string]string{
"key1": "value 1",
"key2": "value2",
"level": "info",
},
links: map[string]string{},
},
linkTest{
before: map[string]string{
"key1": "value 1",
"key2": "http://localhost",
"level": "info",
},
after: map[string]string{
"key1": "value 1",
"level": "info",
},
links: map[string]string{
"key2": "http://localhost",
},
},
linkTest{
before: map[string]string{
"key1": "value 1",
"key2": "https://example.com/abc",
"level": "info",
},
after: map[string]string{
"key1": "value 1",
"level": "info",
},
links: map[string]string{
"key2": "https://example.com/abc",
},
},
linkTest{
before: map[string]string{
"key1": "value 1",
"key2": "file://example/abc",
"level": "info",
},
after: map[string]string{
"key1": "value 1",
"key2": "file://example/abc",
"level": "info",
},
links: map[string]string{},
},
linkTest{
before: map[string]string{
"key1": "value 1",
"key2": "ftp://example/abc",
"level": "info",
},
after: map[string]string{
"key1": "value 1",
"level": "info",
},
links: map[string]string{
"key2": "ftp://example/abc",
},
},
}
func TestDetectLinks(t *testing.T) {
for _, testCase := range linkTests {
after, links := transform.DetectLinks(testCase.before)
if !reflect.DeepEqual(after, testCase.after) {
t.Errorf("DetectLinks returned invalid annotation map, expected %v, got %v", testCase.after, after)
}
if !reflect.DeepEqual(links, testCase.links) {
t.Errorf("DetectLinks returned invalid link map, expected %v, got %v", testCase.links, links)
}
}
}

View File

@@ -163,8 +163,14 @@ func TestAlerts(t *testing.T) {
}
for _, ag := range ur.AlertGroups {
for _, a := range ag.Alerts {
if len(a.Links) != 1 {
t.Errorf("Invalid number of links, got %d, expected 1, %v", len(a.Links), a)
linkCount := 0
for _, annotation := range a.Annotations {
if annotation.IsLink {
linkCount++
}
}
if linkCount != 1 {
t.Errorf("Invalid number of links, got %d, expected 1, %v", linkCount, a)
}
if len(a.Alertmanager) == 0 {
t.Errorf("Alertmanager instance list is empty, %v", a)