Merge branch 'v8.4.x' of github.com:grafana/grafana into v8.4.x
This commit is contained in:
@@ -16,6 +16,8 @@ on:
|
||||
required: true
|
||||
metricsWriteAPIKey:
|
||||
required: true
|
||||
env:
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS: false
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -80,7 +82,7 @@ jobs:
|
||||
ref: main
|
||||
- uses: actions/setup-node@v2.5.1
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- name: Install Actions
|
||||
run: npm install --production --prefix ./actions
|
||||
- name: Run bump version (manually invoked)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
<!-- 8.4.2 START -->
|
||||
|
||||
# 8.4.2 (2022-02-23)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **OAuth:** Add setting to skip org assignment for external users. [#34834](https://github.com/grafana/grafana/pull/34834), [@baez90](https://github.com/baez90)
|
||||
- **Tracing:** Add option to map tag names to log label names in trace to logs settings. [#45178](https://github.com/grafana/grafana/pull/45178), [@connorlindsey](https://github.com/connorlindsey)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Explore:** Fix closing split pane when logs panel is used. [#45602](https://github.com/grafana/grafana/pull/45602), [@ifrost](https://github.com/ifrost)
|
||||
|
||||
<!-- 8.4.2 END -->
|
||||
<!-- 8.4.1 START -->
|
||||
|
||||
# 8.4.1 (2022-02-18)
|
||||
|
||||
@@ -14,7 +14,7 @@ Grafana has default and custom configuration files. You can customize your Grafa
|
||||
|
||||
## Configuration file location
|
||||
|
||||
The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change the location in this file.
|
||||
The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change this file.
|
||||
|
||||
Depending on your OS, your custom configuration file is either the `$WORKING_DIR/conf/defaults.ini` file or the `/usr/local/etc/grafana/grafana.ini` file. The custom configuration file path can be overridden using the `--config` parameter.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Labels are key-value pairs that contain information about, and are used to uniqu
|
||||
### How are labels used?
|
||||
|
||||
- The complete set of labels for an alert is what uniquely identifies an alert within Grafana Alerts.
|
||||
- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notification-policies/" >}}).
|
||||
- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notifications/_index.md" >}}).
|
||||
- The alerting UI displays labels for every alert instance generated by the evaluation of that rule.
|
||||
- Contact points can access labels to dynamically generate notifications that contain information specific to the alert that is resulting in a notification.
|
||||
- Labels can be added to an [alerting rule]({{< relref "../alerting-rules/" >}}). These manually configured labels are able to use template functions and reference other labels. Labels added to an alerting rule here take precedence in the event of a collision between labels.
|
||||
|
||||
@@ -9,7 +9,7 @@ weight = 450
|
||||
|
||||
Notification policies determine how alerts are routed to contact points. Policies have a tree structure, where each policy can have one or more child policies. Each policy, except for the root policy, can also match specific alert labels. Each alert is evaluated by the root policy and subsequently by each child policy. If you enable the `Continue matching subsequent sibling nodes` option is enabled for a specific policy, then evaluation continues even after one or more matches. A parent policy’s configuration settings and contact point information govern the behavior of an alert that does not match any of the child policies. A root policy governs any alert that does not match a specific policy.
|
||||
|
||||
You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}).
|
||||
You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}).
|
||||
|
||||
## Grouping
|
||||
|
||||
@@ -33,7 +33,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en
|
||||
1. Click **Notification policies**.
|
||||
1. From the **Alertmanager** dropdown, select an external Alertmanager. By default, the Grafana Alertmanager is selected.
|
||||
1. In the Root policy section, click **Edit** (pen icon).
|
||||
1. In **Default contact point**, update the [contact point]({{< relref "./contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy.
|
||||
1. In **Default contact point**, update the [contact point]({{< relref "../contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy.
|
||||
1. In **Group by**, choose labels to group alerts by. If multiple alerts are matched for this policy, then they are grouped by these labels. A notification is sent per group. If the field is empty (default), then all notifications are sent in a single group. Use a special label `...` to group alerts by all labels (which effectively disables grouping).
|
||||
1. In **Timing options**, select from the following options:
|
||||
- **Group wait** Time to wait to buffer alerts of the same group before sending an initial notification. Default is 30 seconds.
|
||||
@@ -48,7 +48,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en
|
||||
1. From the **Alertmanager** dropdown, select an Alertmanager. By default, the Grafana Alertmanager is selected.
|
||||
1. To add a top level specific policy, go to the **Specific routing** section and click **New specific policy**.
|
||||
1. In **Matching labels** section, add one or more rules for matching alert labels. For more information, see ["How label matching works"](#how-label-matching-works).
|
||||
1. In **Contact point**, add the [contact point]({{< relref "./contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies.
|
||||
1. In **Contact point**, add the [contact point]({{< relref "../contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies.
|
||||
1. Optionally, enable **Continue matching subsequent sibling nodes** to continue matching nested policies even after the alert matched the parent policy. When this option is enabled, you can get more than one notification. Use it to send notification to a catch-all contact point as well as to one of more specific contact points handled by nested policies.
|
||||
1. Optionally, enable **Override grouping** to specify the same grouping as the root policy. If this option is not enabled, the root policy grouping is used.
|
||||
1. Optionally, enable **Override general timings** to override the timing options configured in the group notification policy.
|
||||
|
||||
@@ -11,7 +11,7 @@ A mute timing is a recurring interval of time when no new notifications for a po
|
||||
|
||||
Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created.
|
||||
|
||||
You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}).
|
||||
You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}).
|
||||
|
||||
## Mute timings vs silences
|
||||
|
||||
|
||||
@@ -291,16 +291,15 @@ When configured, Grafana will pass the user's token to the plugin in an Authoriz
|
||||
|
||||
```go
|
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
for _, q := range req.Queries {
|
||||
token := strings.Fields(q.Headers.Get("Authorization"))
|
||||
token := strings.Fields(req.Headers["Authorization"])
|
||||
var (
|
||||
tokenType = token[0]
|
||||
accessToken = token[1]
|
||||
)
|
||||
|
||||
var (
|
||||
tokenType = token[0]
|
||||
accessToken = token[1]
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
for _, q := range req.Queries {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ If the data source does not support loading full range log volume histogram, the
|
||||
|
||||
For logs where a level label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with an unknown log level.
|
||||
|
||||
> **Tip:** If you use Loki data source and the "level" is in you log content, try to use parsers (JSON, logfmt, regex,..) to extract level information into level label that is used to determine log level.
|
||||
> **Tip:** If you use Loki data source and the "level" is in your log-line, use parsers (JSON, logfmt, regex,..) to extract the level information into a level label that is used to determine log level. This will allow the histogram to show the various log levels in separate bars.
|
||||
|
||||
**Supported log levels and mapping of log level abbreviation and expressions:**
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Status Codes:
|
||||
|
||||
## Get dashboard version
|
||||
|
||||
`GET /api/dashboards/id/:dashboardId/versions/:id`
|
||||
`GET /api/dashboards/id/:dashboardId/versions/:version`
|
||||
|
||||
Get the dashboard version with the given version, for the dashboard with the given id.
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ weight = 10000
|
||||
Here you can find detailed release notes that list everything that is included in every release as well as notices
|
||||
about deprecations, breaking changes as well as changes that relate to plugin development.
|
||||
|
||||
- [Release notes for 8.4.2]({{< relref "release-notes-8-4-2" >}})
|
||||
- [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}})
|
||||
- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}})
|
||||
- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}})
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.4.2"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.4.2
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **OAuth:** Add setting to skip org assignment for external users. [#34834](https://github.com/grafana/grafana/pull/34834), [@baez90](https://github.com/baez90)
|
||||
- **Tracing:** Add option to map tag names to log label names in trace to logs settings. [#45178](https://github.com/grafana/grafana/pull/45178), [@connorlindsey](https://github.com/connorlindsey)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Explore:** Fix closing split pane when logs panel is used. [#45602](https://github.com/grafana/grafana/pull/45602), [@ifrost](https://github.com/ifrost)
|
||||
@@ -5,7 +5,7 @@ e2e.scenario({
|
||||
itName: 'Tests dashboard links and variables in links',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
skipScenario: true, // Skipped because it was causing many failures in main.
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: 'yBCC3aKGk' });
|
||||
e2e()
|
||||
|
||||
@@ -140,14 +140,14 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
||||
standard: true,
|
||||
reduce: calculateLast,
|
||||
},
|
||||
{ id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
|
||||
{
|
||||
id: ReducerID.firstNotNull,
|
||||
name: 'First',
|
||||
name: 'First *',
|
||||
description: 'First non-null value',
|
||||
standard: true,
|
||||
reduce: calculateFirstNotNull,
|
||||
},
|
||||
{ id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
|
||||
{ id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
|
||||
{ id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
|
||||
{ id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] },
|
||||
|
||||
@@ -229,4 +229,4 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM
|
||||
};
|
||||
|
||||
export const escapeUnescapedString = (string: string) =>
|
||||
string.replace(/\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n'));
|
||||
string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n'));
|
||||
|
||||
@@ -21,24 +21,46 @@ export interface Props extends ButtonProps {
|
||||
const dummyClearFunc = () => {};
|
||||
|
||||
export function ClipboardButton({ onClipboardCopy, onClipboardError, children, getText, ...buttonProps }: Props) {
|
||||
// Can be removed in 9.x
|
||||
const buttonRef = useRef<null | HTMLButtonElement>(null);
|
||||
const copyText = useCallback(() => {
|
||||
const copiedText = getText();
|
||||
const copyTextCallback = useCallback(async () => {
|
||||
const textToCopy = getText();
|
||||
// Can be removed in 9.x
|
||||
const dummyEvent: ClipboardEvent = {
|
||||
action: 'copy',
|
||||
clearSelection: dummyClearFunc,
|
||||
text: copiedText,
|
||||
text: textToCopy,
|
||||
trigger: buttonRef.current!,
|
||||
};
|
||||
navigator.clipboard
|
||||
.writeText(copiedText)
|
||||
.then(() => (onClipboardCopy?.(dummyEvent), () => onClipboardError?.(dummyEvent)));
|
||||
try {
|
||||
await copyText(textToCopy, buttonRef);
|
||||
onClipboardCopy?.(dummyEvent);
|
||||
} catch {
|
||||
onClipboardError?.(dummyEvent);
|
||||
}
|
||||
}, [getText, onClipboardCopy, onClipboardError]);
|
||||
|
||||
return (
|
||||
<Button onClick={copyText} {...buttonProps} ref={buttonRef}>
|
||||
<Button onClick={copyTextCallback} {...buttonProps} ref={buttonRef}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const copyText = async (text: string, buttonRef: React.MutableRefObject<HTMLButtonElement | null>) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// Use a fallback method for browsers/contexts that don't support the Clipboard API.
|
||||
// See https://web.dev/async-clipboard/#feature-detection.
|
||||
const input = document.createElement('input');
|
||||
// Normally we'd append this to the body. However if we're inside a focus manager
|
||||
// from react-aria, we can't focus anything outside of the managed area.
|
||||
// Instead, let's append it to the button. Then we're guaranteed to be able to focus + copy.
|
||||
buttonRef.current?.appendChild(input);
|
||||
input.value = text;
|
||||
input.focus();
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -441,7 +441,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
}
|
||||
|
||||
m.Use(middleware.Recovery(hs.Cfg))
|
||||
m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName))
|
||||
m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName, hs.log))
|
||||
|
||||
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
|
||||
hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public", "/public/views/swagger.html")
|
||||
|
||||
@@ -129,7 +129,7 @@ func (ccc *ConditionsCmd) Execute(ctx context.Context, vars mathexp.Vars) (mathe
|
||||
}
|
||||
|
||||
thisCondFiring := firingCount > 0
|
||||
thisCondNoData := nilReducedCount > 0
|
||||
thisCondNoData := len(querySeriesSet.Values) == nilReducedCount
|
||||
|
||||
if i == 0 {
|
||||
firing = thisCondFiring
|
||||
@@ -144,7 +144,7 @@ func (ccc *ConditionsCmd) Execute(ctx context.Context, vars mathexp.Vars) (mathe
|
||||
noDataFound = noDataFound && thisCondNoData
|
||||
}
|
||||
|
||||
if len(querySeriesSet.Values) == nilReducedCount {
|
||||
if thisCondNoData {
|
||||
matches = append(matches, EvalMatch{
|
||||
Metric: "NoData",
|
||||
})
|
||||
|
||||
@@ -169,6 +169,31 @@ func TestConditionsCmdExecute(t *testing.T) {
|
||||
return v
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single query and single condition - empty series and not empty series",
|
||||
vars: mathexp.Vars{
|
||||
"A": mathexp.Results{
|
||||
Values: []mathexp.Value{
|
||||
valBasedSeries(),
|
||||
valBasedSeries(ptr.Float64(3)),
|
||||
},
|
||||
},
|
||||
},
|
||||
conditionsCmd: &ConditionsCmd{
|
||||
Conditions: []condition{
|
||||
{
|
||||
QueryRefID: "A",
|
||||
Reducer: classicReducer("avg"),
|
||||
Operator: "and",
|
||||
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: .5},
|
||||
},
|
||||
}},
|
||||
resultNumber: func() mathexp.Number {
|
||||
v := valBasedNumber(ptr.Float64(1))
|
||||
v.SetMeta([]EvalMatch{{Value: ptr.Float64(3)}})
|
||||
return v
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single query and two conditions",
|
||||
vars: mathexp.Vars{
|
||||
|
||||
@@ -102,6 +102,12 @@ func TestReducer(t *testing.T) {
|
||||
inputSeries: valBasedSeries(nil, nil, ptr.Float64(3), ptr.Float64(4)),
|
||||
expectedNumber: valBasedNumber(ptr.Float64(2)),
|
||||
},
|
||||
{
|
||||
name: "count_non_null with mixed null/real values",
|
||||
reducer: classicReducer("count_non_null"),
|
||||
inputSeries: valBasedSeries(nil, nil, ptr.Float64(3), ptr.Float64(4)),
|
||||
expectedNumber: valBasedNumber(ptr.Float64(2)),
|
||||
},
|
||||
{
|
||||
name: "count_non_null with no values",
|
||||
reducer: classicReducer("count_non_null"),
|
||||
|
||||
+15
-4
@@ -4,10 +4,12 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func CSRF(loginCookieName string) func(http.Handler) http.Handler {
|
||||
func CSRF(loginCookieName string, logger log.Logger) func(http.Handler) http.Handler {
|
||||
// As per RFC 7231/4.2.2 these methods are idempotent:
|
||||
// (GET is excluded because it may have side effects in some APIs)
|
||||
safeMethods := []string{"HEAD", "OPTIONS", "TRACE"}
|
||||
@@ -27,12 +29,21 @@ func CSRF(loginCookieName string) func(http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
// Otherwise - verify that Origin matches the server origin
|
||||
host := strings.Split(r.Host, ":")[0]
|
||||
netAddr, err := util.SplitHostPortDefault(r.Host, "", "0") // we ignore the port
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
origin, err := url.Parse(r.Header.Get("Origin"))
|
||||
if err != nil || (origin.String() != "" && origin.Hostname() != host) {
|
||||
if err != nil {
|
||||
logger.Error("error parsing Origin header", "err", err)
|
||||
}
|
||||
if err != nil || netAddr.Host == "" || (origin.String() != "" && origin.Hostname() != netAddr.Host) {
|
||||
http.Error(w, "origin not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMiddlewareCSRF(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cookieName string
|
||||
method string
|
||||
origin string
|
||||
host string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
name: "mismatched origin and host is forbidden",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
origin: "http://notLocalhost",
|
||||
host: "localhost",
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "mismatched origin and host is NOT forbidden with a 'Safe Method'",
|
||||
cookieName: "foo",
|
||||
method: "TRACE",
|
||||
origin: "http://notLocalhost",
|
||||
host: "localhost",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "mismatched origin and host is NOT forbidden without a cookie",
|
||||
cookieName: "",
|
||||
method: "GET",
|
||||
origin: "http://notLocalhost",
|
||||
host: "localhost",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "malformed host is a bad request",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
host: "localhost:80:80",
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "host works without port",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
host: "localhost",
|
||||
origin: "http://localhost",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "port does not have to match",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
host: "localhost:80",
|
||||
origin: "http://localhost:3000",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "IPv6 host works with port",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
host: "[::1]:3000",
|
||||
origin: "http://[::1]:3000",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "IPv6 host (with longer address) works with port",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
host: "[2001:db8::1]:3000",
|
||||
origin: "http://[2001:db8::1]:3000",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "IPv6 host (with longer address) works without port",
|
||||
cookieName: "foo",
|
||||
method: "GET",
|
||||
host: "[2001:db8::1]",
|
||||
origin: "http://[2001:db8::1]",
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rr := csrfScenario(t, tt.cookieName, tt.method, tt.origin, tt.host)
|
||||
require.Equal(t, tt.code, rr.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func csrfScenario(t *testing.T, cookieName, method, origin, host string) *httptest.ResponseRecorder {
|
||||
req, err := http.NewRequest(method, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: cookieName,
|
||||
})
|
||||
|
||||
// Note: Not sure where host header populates req.Host, or how that works.
|
||||
req.Host = host
|
||||
req.Header.Set("HOST", host)
|
||||
|
||||
req.Header.Set("ORIGIN", origin)
|
||||
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := CSRF(cookieName, log.New())(testHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
@@ -459,6 +459,14 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
||||
PropertyName: "url",
|
||||
Secure: true,
|
||||
},
|
||||
{ // New in 8.4.
|
||||
Label: "Endpoint URL",
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypeText,
|
||||
Description: "Optionally provide a custom Slack message API endpoint for non-webhook requests, default is https://slack.com/api/chat.postMessage",
|
||||
Placeholder: "Slack endpoint url",
|
||||
PropertyName: "endpointUrl",
|
||||
},
|
||||
{ // New in 8.0.
|
||||
Label: "Title",
|
||||
Element: alerting.ElementTypeInput,
|
||||
@@ -589,7 +597,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
||||
},
|
||||
{
|
||||
Type: "webhook",
|
||||
Name: "webhook",
|
||||
Name: "Webhook",
|
||||
Description: "Sends HTTP POST request to a URL",
|
||||
Heading: "Webhook settings",
|
||||
Options: []alerting.NotifierOption{
|
||||
|
||||
@@ -99,7 +99,7 @@ Labels:
|
||||
Annotations:
|
||||
- ann1 = annv1
|
||||
Source: http://localhost/alert1
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1
|
||||
Dashboard: http://localhost/grafana/d/dbuid123
|
||||
Panel: http://localhost/grafana/d/dbuid123?viewPanel=puid123
|
||||
|
||||
@@ -110,7 +110,7 @@ Labels:
|
||||
Annotations:
|
||||
- ann1 = annv2
|
||||
Source: http://localhost/alert2
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2
|
||||
|
||||
|
||||
**Resolved**
|
||||
@@ -122,7 +122,7 @@ Labels:
|
||||
Annotations:
|
||||
- ann1 = annv3
|
||||
Source: http://localhost/alert3
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval3
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval3
|
||||
Dashboard: http://localhost/grafana/d/dbuid456
|
||||
Panel: http://localhost/grafana/d/dbuid456?viewPanel=puid456
|
||||
|
||||
@@ -133,7 +133,7 @@ Labels:
|
||||
Annotations:
|
||||
- ann1 = annv4
|
||||
Source: http://localhost/alert4
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval4
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval4
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -150,7 +150,7 @@ Annotations:
|
||||
|
||||
Source: http://localhost/alert1
|
||||
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1
|
||||
|
||||
Dashboard: http://localhost/grafana/d/dbuid123
|
||||
|
||||
@@ -168,7 +168,7 @@ Annotations:
|
||||
|
||||
Source: http://localhost/alert2
|
||||
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2
|
||||
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ Annotations:
|
||||
|
||||
Source: http://localhost/alert3
|
||||
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval3
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval3
|
||||
|
||||
Dashboard: http://localhost/grafana/d/dbuid456
|
||||
|
||||
@@ -203,7 +203,7 @@ Annotations:
|
||||
|
||||
Source: http://localhost/alert4
|
||||
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval4
|
||||
Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval4
|
||||
|
||||
|
||||
`,
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestDingdingNotifier(t *testing.T) {
|
||||
"msgtype": "link",
|
||||
"link": map[string]interface{}{
|
||||
"messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%2Falerting%2Flist",
|
||||
"text": "**Firing**\n\nValue: 1234\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"text": "**Firing**\n\nValue: 1234\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"title": "[FIRING:1] (val1)",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestDiscordNotifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expMsg: map[string]interface{}{
|
||||
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"embeds": []interface{}{map[string]interface{}{
|
||||
"color": 1.4037554e+07,
|
||||
"footer": map[string]interface{}{
|
||||
@@ -123,7 +123,7 @@ func TestDiscordNotifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expMsg: map[string]interface{}{
|
||||
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"embeds": []interface{}{map[string]interface{}{
|
||||
"color": 1.4037554e+07,
|
||||
"footer": map[string]interface{}{
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestEmailNotifier(t *testing.T) {
|
||||
Labels: template.KV{"alertname": "AlwaysFiring", "severity": "warning"},
|
||||
Annotations: template.KV{"runbook_url": "http://fix.me"},
|
||||
Fingerprint: "15a37193dce72bab",
|
||||
SilenceURL: "http://localhost/base/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DAlwaysFiring%2Cseverity%3Dwarning",
|
||||
SilenceURL: "http://localhost/base/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DAlwaysFiring&matcher=severity%3Dwarning",
|
||||
DashboardURL: "http://localhost/base/d/abc",
|
||||
PanelURL: "http://localhost/base/d/abc?viewPanel=5",
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestGoogleChatNotifier(t *testing.T) {
|
||||
Widgets: []widget{
|
||||
textParagraphWidget{
|
||||
Text: text{
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
},
|
||||
},
|
||||
buttonWidget{
|
||||
@@ -117,7 +117,7 @@ func TestGoogleChatNotifier(t *testing.T) {
|
||||
Widgets: []widget{
|
||||
textParagraphWidget{
|
||||
Text: text{
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
},
|
||||
},
|
||||
buttonWidget{
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestKafkaNotifier(t *testing.T) {
|
||||
"client": "Grafana",
|
||||
"client_url": "http://localhost/alerting/list",
|
||||
"description": "[FIRING:1] (val1)",
|
||||
"details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733"
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func TestKafkaNotifier(t *testing.T) {
|
||||
"client": "Grafana",
|
||||
"client_url": "http://localhost/alerting/list",
|
||||
"description": "[FIRING:2] ",
|
||||
"details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
"details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
"incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestLineNotifier(t *testing.T) {
|
||||
"Authorization": "Bearer sometoken",
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
},
|
||||
expMsg: "message=%5BFIRING%3A1%5D++%28val1%29%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A",
|
||||
expMsg: "message=%5BFIRING%3A1%5D++%28val1%29%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A",
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Multiple alerts",
|
||||
@@ -68,7 +68,7 @@ func TestLineNotifier(t *testing.T) {
|
||||
"Authorization": "Bearer sometoken",
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
},
|
||||
expMsg: "message=%5BFIRING%3A2%5D++%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval2%0A",
|
||||
expMsg: "message=%5BFIRING%3A2%5D++%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval2%0A",
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Token missing",
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestOpsgenieNotifier(t *testing.T) {
|
||||
},
|
||||
expMsg: `{
|
||||
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"details": {
|
||||
"url": "http://localhost/alerting/list"
|
||||
},
|
||||
@@ -69,7 +69,7 @@ func TestOpsgenieNotifier(t *testing.T) {
|
||||
},
|
||||
expMsg: `{
|
||||
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n",
|
||||
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n",
|
||||
"details": {
|
||||
"url": "http://localhost/alerting/list"
|
||||
},
|
||||
@@ -94,7 +94,7 @@ func TestOpsgenieNotifier(t *testing.T) {
|
||||
},
|
||||
expMsg: `{
|
||||
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n",
|
||||
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n",
|
||||
"details": {
|
||||
"alertname": "alert1",
|
||||
"lbl1": "val1",
|
||||
@@ -126,7 +126,7 @@ func TestOpsgenieNotifier(t *testing.T) {
|
||||
},
|
||||
expMsg: `{
|
||||
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
"description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
"details": {
|
||||
"alertname": "alert1",
|
||||
"url": "http://localhost/alerting/list"
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestPagerdutyNotifier(t *testing.T) {
|
||||
Component: "Grafana",
|
||||
Group: "default",
|
||||
CustomDetails: map[string]string{
|
||||
"firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"num_firing": "1",
|
||||
"num_resolved": "0",
|
||||
"resolved": "",
|
||||
@@ -105,7 +105,7 @@ func TestPagerdutyNotifier(t *testing.T) {
|
||||
Component: "My Grafana",
|
||||
Group: "my_group",
|
||||
CustomDetails: map[string]string{
|
||||
"firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
"firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
"num_firing": "2",
|
||||
"num_resolved": "0",
|
||||
"resolved": "",
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
"title": "[FIRING:1] (val1)",
|
||||
"url": "http://localhost/alerting/list",
|
||||
"url_title": "Show alert rule",
|
||||
"message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"html": "1",
|
||||
},
|
||||
expMsgError: nil,
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestSensuGoNotifier(t *testing.T) {
|
||||
"ruleURL": "http://localhost/alerting/list",
|
||||
},
|
||||
},
|
||||
"output": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"output": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"issued": timeNow().Unix(),
|
||||
"interval": 86400,
|
||||
"status": 2,
|
||||
|
||||
@@ -52,9 +52,11 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
|
||||
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
|
||||
}
|
||||
|
||||
endpointURL := model.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint)
|
||||
|
||||
slackURL := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString())
|
||||
if slackURL == "" {
|
||||
slackURL = SlackAPIEndpoint
|
||||
slackURL = endpointURL
|
||||
}
|
||||
apiURL, err := url.Parse(slackURL)
|
||||
if err != nil {
|
||||
@@ -62,7 +64,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
|
||||
}
|
||||
|
||||
recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString())
|
||||
if recipient == "" && apiURL.String() == SlackAPIEndpoint {
|
||||
if recipient == "" && apiURL.String() == endpointURL {
|
||||
return nil, receiverInitError{Cfg: *model,
|
||||
Reason: "recipient must be specified when using the Slack chat API",
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestSlackNotifier(t *testing.T) {
|
||||
{
|
||||
Title: "[FIRING:1] (val1)",
|
||||
TitleLink: "http://localhost/alerting/list",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
Fallback: "[FIRING:1] (val1)",
|
||||
Fields: nil,
|
||||
Footer: "Grafana v" + setting.BuildVersion,
|
||||
@@ -94,7 +94,7 @@ func TestSlackNotifier(t *testing.T) {
|
||||
{
|
||||
Title: "[FIRING:1] (val1)",
|
||||
TitleLink: "http://localhost/alerting/list",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n",
|
||||
Fallback: "[FIRING:1] (val1)",
|
||||
Fields: nil,
|
||||
Footer: "Grafana v" + setting.BuildVersion,
|
||||
@@ -136,7 +136,7 @@ func TestSlackNotifier(t *testing.T) {
|
||||
{
|
||||
Title: "2 firing, 0 resolved",
|
||||
TitleLink: "http://localhost/alerting/list",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
Fallback: "2 firing, 0 resolved",
|
||||
Fields: nil,
|
||||
Footer: "Grafana v" + setting.BuildVersion,
|
||||
@@ -160,6 +160,42 @@ func TestSlackNotifier(t *testing.T) {
|
||||
}`,
|
||||
expInitError: `failed to validate receiver "slack_testing" of type "slack": recipient must be specified when using the Slack chat API`,
|
||||
},
|
||||
{
|
||||
name: "Custom endpoint url",
|
||||
settings: `{
|
||||
"token": "1234",
|
||||
"recipient": "#testchannel",
|
||||
"endpointUrl": "https://slack-custom.com/api/",
|
||||
"icon_emoji": ":emoji:"
|
||||
}`,
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: &slackMessage{
|
||||
Channel: "#testchannel",
|
||||
Username: "Grafana",
|
||||
IconEmoji: ":emoji:",
|
||||
Attachments: []attachment{
|
||||
{
|
||||
Title: "[FIRING:1] (val1)",
|
||||
TitleLink: "http://localhost/alerting/list",
|
||||
Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n",
|
||||
Fallback: "[FIRING:1] (val1)",
|
||||
Fields: nil,
|
||||
Footer: "Grafana v" + setting.BuildVersion,
|
||||
FooterIcon: "https://grafana.com/assets/img/fav32.png",
|
||||
Color: "#D63232",
|
||||
Ts: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsgError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -196,6 +232,12 @@ func TestSlackNotifier(t *testing.T) {
|
||||
_ = request.Body.Close()
|
||||
}()
|
||||
|
||||
url := settingsJSON.Get("url").MustString()
|
||||
if len(url) == 0 {
|
||||
endpointUrl := settingsJSON.Get("endpointUrl").MustString(SlackAPIEndpoint)
|
||||
require.Equal(t, endpointUrl, request.URL.String())
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(request.Body)
|
||||
require.NoError(t, err)
|
||||
body = string(b)
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestTeamsNotifier(t *testing.T) {
|
||||
"sections": []map[string]interface{}{
|
||||
{
|
||||
"title": "Details",
|
||||
"text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
},
|
||||
},
|
||||
"potentialAction": []map[string]interface{}{
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
expMsg: map[string]string{
|
||||
"chat_id": "someid",
|
||||
"parse_mode": "html",
|
||||
"text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
},
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
@@ -75,7 +75,7 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
expMsg: map[string]string{
|
||||
"chat_id": "someid",
|
||||
"parse_mode": "html",
|
||||
"text": "__Custom Firing__\n2 Firing\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
"text": "__Custom Firing__\n2 Firing\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
},
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
|
||||
@@ -99,7 +99,15 @@ func extendAlert(alert template.Alert, externalURL string, logger log.Logger) *E
|
||||
}
|
||||
sort.Strings(matchers)
|
||||
u.Path = path.Join(externalPath, "/alerting/silence/new")
|
||||
u.RawQuery = "alertmanager=grafana&matchers=" + url.QueryEscape(strings.Join(matchers, ","))
|
||||
|
||||
query := make(url.Values)
|
||||
query.Add("alertmanager", "grafana")
|
||||
for _, matcher := range matchers {
|
||||
query.Add("matcher", matcher)
|
||||
}
|
||||
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
extended.SilenceURL = u.String()
|
||||
|
||||
return extended
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestThreemaNotifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321",
|
||||
expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321",
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Multiple alerts",
|
||||
@@ -67,7 +67,7 @@ func TestThreemaNotifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval2%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321",
|
||||
expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval2%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321",
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Invalid gateway id",
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestVictoropsNotifier(t *testing.T) {
|
||||
"entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"message_type": "CRITICAL",
|
||||
"monitoring_tool": "Grafana v" + setting.BuildVersion,
|
||||
"state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
},
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
@@ -72,7 +72,7 @@ func TestVictoropsNotifier(t *testing.T) {
|
||||
"entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"message_type": "CRITICAL",
|
||||
"monitoring_tool": "Grafana v" + setting.BuildVersion,
|
||||
"state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
"state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
},
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
Fingerprint: "fac0861a85de433a",
|
||||
DashboardURL: "http://localhost/d/abcd",
|
||||
PanelURL: "http://localhost/d/abcd?viewPanel=efgh",
|
||||
SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1",
|
||||
SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1",
|
||||
},
|
||||
},
|
||||
GroupLabels: template.KV{
|
||||
@@ -87,7 +87,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
GroupKey: "alertname",
|
||||
Title: "[FIRING:1] (val1)",
|
||||
State: "alerting",
|
||||
Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
OrgID: orgID,
|
||||
},
|
||||
expMsgError: nil,
|
||||
@@ -137,7 +137,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
"ann1": "annv1",
|
||||
},
|
||||
Fingerprint: "fac0861a85de433a",
|
||||
SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1",
|
||||
SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1",
|
||||
}, {
|
||||
Status: "firing",
|
||||
Labels: template.KV{
|
||||
@@ -148,7 +148,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
"ann1": "annv2",
|
||||
},
|
||||
Fingerprint: "fab6861a85d5eeb5",
|
||||
SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2",
|
||||
SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2",
|
||||
},
|
||||
},
|
||||
GroupLabels: template.KV{
|
||||
@@ -165,7 +165,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
TruncatedAlerts: 1,
|
||||
Title: "[FIRING:2] ",
|
||||
State: "alerting",
|
||||
Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n",
|
||||
Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
|
||||
OrgID: orgID,
|
||||
},
|
||||
expMsgError: nil,
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestWeComNotifier(t *testing.T) {
|
||||
},
|
||||
expMsg: map[string]interface{}{
|
||||
"markdown": map[string]interface{}{
|
||||
"content": "# [FIRING:1] (val1)\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n\n",
|
||||
"content": "# [FIRING:1] (val1)\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n\n",
|
||||
},
|
||||
"msgtype": "markdown",
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ type schedule struct {
|
||||
|
||||
clock clock.Clock
|
||||
|
||||
heartbeat *alerting.Ticker
|
||||
ticker *alerting.Ticker
|
||||
|
||||
// evalApplied is only used for tests: test code can set it to non-nil
|
||||
// function, and then it'll be called from the event loop whenever the
|
||||
@@ -130,7 +130,7 @@ func NewScheduler(cfg SchedulerCfg, expressionService *expr.Service, appURL *url
|
||||
clock: cfg.C,
|
||||
baseInterval: cfg.BaseInterval,
|
||||
log: cfg.Logger,
|
||||
heartbeat: ticker,
|
||||
ticker: ticker,
|
||||
evalAppliedFunc: cfg.EvalAppliedFunc,
|
||||
stopAppliedFunc: cfg.StopAppliedFunc,
|
||||
evaluator: cfg.Evaluator,
|
||||
@@ -157,7 +157,7 @@ func (sch *schedule) Pause() error {
|
||||
if sch == nil {
|
||||
return fmt.Errorf("scheduler is not initialised")
|
||||
}
|
||||
sch.heartbeat.Pause()
|
||||
sch.ticker.Pause()
|
||||
sch.log.Info("alert rule scheduler paused", "now", sch.clock.Now())
|
||||
return nil
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func (sch *schedule) Unpause() error {
|
||||
if sch == nil {
|
||||
return fmt.Errorf("scheduler is not initialised")
|
||||
}
|
||||
sch.heartbeat.Unpause()
|
||||
sch.ticker.Unpause()
|
||||
sch.log.Info("alert rule scheduler unpaused", "now", sch.clock.Now())
|
||||
return nil
|
||||
}
|
||||
@@ -367,8 +367,12 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error {
|
||||
dispatcherGroup, ctx := errgroup.WithContext(ctx)
|
||||
for {
|
||||
select {
|
||||
case tick := <-sch.heartbeat.C:
|
||||
start := time.Now()
|
||||
case tick := <-sch.ticker.C:
|
||||
// We use Round(0) on the start time to remove the monotonic clock.
|
||||
// This is required as ticks from the ticker and time.Now() can have
|
||||
// a monotonic clock that when subtracted do not represent the delta
|
||||
// in wall clock time.
|
||||
start := time.Now().Round(0)
|
||||
sch.metrics.BehindSeconds.Set(start.Sub(tick).Seconds())
|
||||
|
||||
tickNum := tick.Unix() / int64(sch.baseInterval.Seconds())
|
||||
@@ -468,7 +472,7 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRuleKey, evalCh <-chan *evalContext, updateCh <-chan struct{}) error {
|
||||
func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRuleKey, evalCh <-chan *evaluation, updateCh <-chan struct{}) error {
|
||||
logger := sch.log.New("uid", key.UID, "org", key.OrgID)
|
||||
logger.Debug("alert rule routine started")
|
||||
|
||||
@@ -541,16 +545,16 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRul
|
||||
return q.Result, nil
|
||||
}
|
||||
|
||||
evaluate := func(ctx context.Context, alertRule *models.AlertRule, attempt int64, evalCtx *evalContext) error {
|
||||
logger := logger.New("version", alertRule.Version, "attempt", attempt, "now", evalCtx.now)
|
||||
evaluate := func(ctx context.Context, r *models.AlertRule, attempt int64, e *evaluation) error {
|
||||
logger := logger.New("version", r.Version, "attempt", attempt, "now", e.scheduledAt)
|
||||
start := sch.clock.Now()
|
||||
|
||||
condition := models.Condition{
|
||||
Condition: alertRule.Condition,
|
||||
OrgID: alertRule.OrgID,
|
||||
Data: alertRule.Data,
|
||||
Condition: r.Condition,
|
||||
OrgID: r.OrgID,
|
||||
Data: r.Data,
|
||||
}
|
||||
results, err := sch.evaluator.ConditionEval(&condition, evalCtx.now, sch.expressionService)
|
||||
results, err := sch.evaluator.ConditionEval(&condition, e.scheduledAt, sch.expressionService)
|
||||
dur := sch.clock.Now().Sub(start)
|
||||
evalTotal.Inc()
|
||||
evalDuration.Observe(dur.Seconds())
|
||||
@@ -562,7 +566,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRul
|
||||
}
|
||||
logger.Debug("alert rule evaluated", "results", results, "duration", dur)
|
||||
|
||||
processedStates := sch.stateManager.ProcessEvalResults(ctx, alertRule, results)
|
||||
processedStates := sch.stateManager.ProcessEvalResults(ctx, r, results)
|
||||
sch.saveAlertStates(ctx, processedStates)
|
||||
alerts := FromAlertStateToPostableAlerts(processedStates, sch.stateManager, sch.appURL)
|
||||
|
||||
@@ -616,7 +620,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRul
|
||||
evalRunning = true
|
||||
defer func() {
|
||||
evalRunning = false
|
||||
sch.evalApplied(key, ctx.now)
|
||||
sch.evalApplied(key, ctx.scheduledAt)
|
||||
}()
|
||||
|
||||
err := retryIfError(func(attempt int64) error {
|
||||
@@ -741,7 +745,7 @@ func (r *alertRuleRegistry) keyMap() map[models.AlertRuleKey]struct{} {
|
||||
}
|
||||
|
||||
type alertRuleInfo struct {
|
||||
evalCh chan *evalContext
|
||||
evalCh chan *evaluation
|
||||
updateCh chan struct{}
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
@@ -749,15 +753,15 @@ type alertRuleInfo struct {
|
||||
|
||||
func newAlertRuleInfo(parent context.Context) *alertRuleInfo {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
return &alertRuleInfo{evalCh: make(chan *evalContext), updateCh: make(chan struct{}), ctx: ctx, stop: cancel}
|
||||
return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan struct{}), ctx: ctx, stop: cancel}
|
||||
}
|
||||
|
||||
// eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped
|
||||
func (a *alertRuleInfo) eval(t time.Time, version int64) bool {
|
||||
select {
|
||||
case a.evalCh <- &evalContext{
|
||||
now: t,
|
||||
version: version,
|
||||
case a.evalCh <- &evaluation{
|
||||
scheduledAt: t,
|
||||
version: version,
|
||||
}:
|
||||
return true
|
||||
case <-a.ctx.Done():
|
||||
@@ -775,16 +779,16 @@ func (a *alertRuleInfo) update() bool {
|
||||
}
|
||||
}
|
||||
|
||||
type evalContext struct {
|
||||
now time.Time
|
||||
version int64
|
||||
type evaluation struct {
|
||||
scheduledAt time.Time
|
||||
version int64
|
||||
}
|
||||
|
||||
// overrideCfg is only used on tests.
|
||||
func (sch *schedule) overrideCfg(cfg SchedulerCfg) {
|
||||
sch.clock = cfg.C
|
||||
sch.baseInterval = cfg.BaseInterval
|
||||
sch.heartbeat = alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
|
||||
sch.ticker = alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
|
||||
sch.evalAppliedFunc = cfg.EvalAppliedFunc
|
||||
sch.stopAppliedFunc = cfg.StopAppliedFunc
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
for _, evalState := range normalStates {
|
||||
// TODO rewrite when we are able to mock/fake state manager
|
||||
t.Run(fmt.Sprintf("when rule evaluation happens (evaluation state %s)", evalState), func(t *testing.T) {
|
||||
evalChan := make(chan *evalContext)
|
||||
evalChan := make(chan *evaluation)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
sch, ruleStore, instanceStore, _, reg := createSchedule(evalAppliedChan)
|
||||
|
||||
@@ -375,9 +375,9 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
|
||||
expectedTime := time.UnixMicro(rand.Int63())
|
||||
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
|
||||
actualTime := waitForTimeChannel(t, evalAppliedChan)
|
||||
@@ -467,7 +467,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
err := sch.ruleRoutine(ctx, models.AlertRuleKey{}, make(chan *evalContext), make(chan struct{}))
|
||||
err := sch.ruleRoutine(ctx, models.AlertRuleKey{}, make(chan *evaluation), make(chan struct{}))
|
||||
stoppedChan <- err
|
||||
}()
|
||||
|
||||
@@ -478,7 +478,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should fetch rule from database only if new version is greater than current", func(t *testing.T) {
|
||||
evalChan := make(chan *evalContext)
|
||||
evalChan := make(chan *evaluation)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -493,9 +493,9 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
}()
|
||||
|
||||
expectedTime := time.UnixMicro(rand.Int63())
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
|
||||
actualTime := waitForTimeChannel(t, evalAppliedChan)
|
||||
@@ -508,9 +508,9 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
|
||||
// and call with new version
|
||||
expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second)
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: newRule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: expectedTime,
|
||||
version: newRule.Version,
|
||||
}
|
||||
|
||||
actualTime = waitForTimeChannel(t, evalAppliedChan)
|
||||
@@ -531,7 +531,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should not fetch rule if version is equal or less than current", func(t *testing.T) {
|
||||
evalChan := make(chan *evalContext)
|
||||
evalChan := make(chan *evaluation)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
sch, ruleStore, _, _, _ := createSchedule(evalAppliedChan)
|
||||
@@ -545,9 +545,9 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
}()
|
||||
|
||||
expectedTime := time.UnixMicro(rand.Int63())
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
|
||||
actualTime := waitForTimeChannel(t, evalAppliedChan)
|
||||
@@ -555,17 +555,17 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
|
||||
// try again with the same version
|
||||
expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second)
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
actualTime = waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
|
||||
expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second)
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version - 1,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: expectedTime,
|
||||
version: rule.Version - 1,
|
||||
}
|
||||
actualTime = waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
@@ -582,7 +582,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
|
||||
t.Run("when update channel is not empty", func(t *testing.T) {
|
||||
t.Run("should fetch the alert rule from database", func(t *testing.T) {
|
||||
evalChan := make(chan *evalContext)
|
||||
evalChan := make(chan *evaluation)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
updateChan := make(chan struct{})
|
||||
|
||||
@@ -612,9 +612,9 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
require.Equal(t, rule.OrgID, m.OrgID)
|
||||
|
||||
// now call evaluation loop to make sure that the rule was persisted
|
||||
evalChan <- &evalContext{
|
||||
now: time.UnixMicro(rand.Int63()),
|
||||
version: rule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: time.UnixMicro(rand.Int63()),
|
||||
version: rule.Version,
|
||||
}
|
||||
waitForTimeChannel(t, evalAppliedChan)
|
||||
|
||||
@@ -637,7 +637,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
go func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
_ = sch.ruleRoutine(ctx, rule.GetKey(), make(chan *evalContext), updateChan)
|
||||
_ = sch.ruleRoutine(ctx, rule.GetKey(), make(chan *evaluation), updateChan)
|
||||
}()
|
||||
|
||||
ruleStore.hook = func(cmd interface{}) error {
|
||||
@@ -677,7 +677,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
return len(s.Alertmanagers()) == 1
|
||||
}, 20*time.Second, 200*time.Millisecond, "external Alertmanager was not discovered.")
|
||||
|
||||
evalChan := make(chan *evalContext)
|
||||
evalChan := make(chan *evaluation)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
updateChan := make(chan struct{})
|
||||
|
||||
@@ -783,7 +783,7 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
return len(s.Alertmanagers()) == 1
|
||||
}, 20*time.Second, 200*time.Millisecond, "external Alertmanager was not discovered.")
|
||||
|
||||
evalChan := make(chan *evalContext)
|
||||
evalChan := make(chan *evaluation)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
sch, ruleStore, _, _, _ := createSchedule(evalAppliedChan)
|
||||
@@ -797,9 +797,9 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
_ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, make(chan struct{}))
|
||||
}()
|
||||
|
||||
evalChan <- &evalContext{
|
||||
now: time.Now(),
|
||||
version: rule.Version,
|
||||
evalChan <- &evaluation{
|
||||
scheduledAt: time.Now(),
|
||||
version: rule.Version,
|
||||
}
|
||||
waitForTimeChannel(t, evalAppliedChan)
|
||||
|
||||
@@ -843,7 +843,7 @@ func TestSchedule_alertRuleInfo(t *testing.T) {
|
||||
select {
|
||||
case ctx := <-r.evalCh:
|
||||
require.Equal(t, version, ctx.version)
|
||||
require.Equal(t, expected, ctx.now)
|
||||
require.Equal(t, expected, ctx.scheduledAt)
|
||||
require.True(t, <-resultCh)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("No message was received on eval channel")
|
||||
|
||||
@@ -438,6 +438,12 @@ func NewFakeAnnotationsRepo() *FakeAnnotationsRepo {
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *FakeAnnotationsRepo) Items() []*annotations.Item {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
return repo.items
|
||||
}
|
||||
|
||||
func (repo *FakeAnnotationsRepo) Len() int {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
|
||||
@@ -5,18 +5,19 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
var ResendDelay = 30 * time.Second
|
||||
@@ -185,7 +186,7 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu
|
||||
|
||||
st.set(currentState)
|
||||
if oldState != currentState.State {
|
||||
go st.createAlertAnnotation(ctx, currentState.State, alertRule, result, oldState)
|
||||
go st.annotateState(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState)
|
||||
}
|
||||
return currentState
|
||||
}
|
||||
@@ -233,18 +234,19 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State {
|
||||
}
|
||||
}
|
||||
|
||||
func (st *Manager) createAlertAnnotation(ctx context.Context, 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(), "oldState", oldState.String())
|
||||
func (st *Manager) annotateState(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) {
|
||||
st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", state.String(), "oldState", previousState.String())
|
||||
|
||||
annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, result.Instance.String(), new.String())
|
||||
labels = removePrivateLabels(labels)
|
||||
annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, labels.String(), state.String())
|
||||
|
||||
item := &annotations.Item{
|
||||
AlertId: alertRule.ID,
|
||||
OrgId: alertRule.OrgID,
|
||||
PrevState: oldState.String(),
|
||||
NewState: new.String(),
|
||||
PrevState: previousState.String(),
|
||||
NewState: state.String(),
|
||||
Text: annotationText,
|
||||
Epoch: result.EvaluatedAt.UnixNano() / int64(time.Millisecond),
|
||||
Epoch: evaluatedAt.UnixNano() / int64(time.Millisecond),
|
||||
}
|
||||
|
||||
dashUid, ok := alertRule.Annotations[ngModels.DashboardUIDAnnotation]
|
||||
@@ -295,6 +297,10 @@ func (st *Manager) staleResultsHandler(ctx context.Context, alertRule *ngModels.
|
||||
if err = st.instanceStore.DeleteAlertInstance(ctx, s.OrgID, s.AlertRuleUID, labelsHash); err != nil {
|
||||
st.log.Error("unable to delete stale instance from database", "error", err.Error(), "orgID", s.OrgID, "alertRuleUID", s.AlertRuleUID, "cacheID", s.CacheId)
|
||||
}
|
||||
|
||||
if s.State == eval.Alerting {
|
||||
st.annotateState(ctx, alertRule, s.Labels, time.Now(), eval.Normal, s.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,3 +308,13 @@ func (st *Manager) staleResultsHandler(ctx context.Context, alertRule *ngModels.
|
||||
func isItStale(lastEval time.Time, intervalSeconds int64) bool {
|
||||
return lastEval.Add(2 * time.Duration(intervalSeconds) * time.Second).Before(time.Now())
|
||||
}
|
||||
|
||||
func removePrivateLabels(labels data.Labels) data.Labels {
|
||||
result := make(data.Labels)
|
||||
for k, v := range labels {
|
||||
if !strings.HasPrefix(k, "__") && !strings.HasSuffix(k, "__") {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +28,52 @@ import (
|
||||
|
||||
var testMetrics = metrics.NewNGAlert(prometheus.NewPedanticRegistry())
|
||||
|
||||
func TestDashboardAnnotations(t *testing.T) {
|
||||
evaluationTime, err := time.Parse("2006-01-02", "2022-01-01")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
_, dbstore := tests.SetupTestEnv(t, 1)
|
||||
|
||||
st := state.NewManager(log.New("test_stale_results_handler"), testMetrics.GetStateMetrics(), nil, dbstore, dbstore)
|
||||
|
||||
fakeAnnoRepo := schedule.NewFakeAnnotationsRepo()
|
||||
annotations.SetRepository(fakeAnnoRepo)
|
||||
|
||||
const mainOrgID int64 = 1
|
||||
|
||||
rule := tests.CreateTestAlertRuleWithLabels(t, ctx, dbstore, 600, mainOrgID, map[string]string{
|
||||
"test1": "testValue1",
|
||||
"test2": "{{ $labels.instance_label }}",
|
||||
})
|
||||
|
||||
st.Warm(ctx)
|
||||
_ = st.ProcessEvalResults(ctx, rule, eval.Results{{
|
||||
Instance: data.Labels{"instance_label": "testValue2"},
|
||||
State: eval.Alerting,
|
||||
EvaluatedAt: evaluationTime,
|
||||
}})
|
||||
|
||||
expected := []string{rule.Title + " {alertname=" + rule.Title + ", instance_label=testValue2, test1=testValue1, test2=testValue2} - Alerting"}
|
||||
sort.Strings(expected)
|
||||
require.Eventuallyf(t, func() bool {
|
||||
var actual []string
|
||||
for _, next := range fakeAnnoRepo.Items() {
|
||||
actual = append(actual, next.Text)
|
||||
}
|
||||
sort.Strings(actual)
|
||||
if len(expected) != len(actual) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(expected); i++ {
|
||||
if expected[i] != actual[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, time.Second, 100*time.Millisecond, "unexpected annotations")
|
||||
}
|
||||
|
||||
func TestProcessEvalResults(t *testing.T) {
|
||||
evaluationTime, err := time.Parse("2006-01-02", "2021-03-25")
|
||||
if err != nil {
|
||||
@@ -503,7 +550,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedAnnotations: 2,
|
||||
expectedAnnotations: 3,
|
||||
expectedStates: map[string]*state.State{
|
||||
`[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`: {
|
||||
AlertRuleUID: "test_alert_rule_uid_2",
|
||||
@@ -529,7 +576,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime,
|
||||
StartsAt: evaluationTime.Add(20 * time.Second),
|
||||
EndsAt: evaluationTime.Add(30 * time.Second).Add(state.ResendDelay * 3),
|
||||
LastEvaluationTime: evaluationTime.Add(30 * time.Second),
|
||||
EvaluationDuration: evaluationDuration,
|
||||
|
||||
@@ -64,7 +64,7 @@ func (a *State) resultAlerting(alertRule *ngModels.AlertRule, result eval.Result
|
||||
case eval.Alerting:
|
||||
a.setEndsAt(alertRule, result)
|
||||
case eval.Pending:
|
||||
if result.EvaluatedAt.Sub(a.StartsAt) > alertRule.For {
|
||||
if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For {
|
||||
a.State = eval.Alerting
|
||||
a.StartsAt = result.EvaluatedAt
|
||||
a.setEndsAt(alertRule, result)
|
||||
|
||||
@@ -52,6 +52,10 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, *
|
||||
|
||||
// CreateTestAlertRule creates a dummy alert definition to be used by the tests.
|
||||
func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64) *models.AlertRule {
|
||||
return CreateTestAlertRuleWithLabels(t, ctx, dbstore, intervalSeconds, orgID, nil)
|
||||
}
|
||||
|
||||
func CreateTestAlertRuleWithLabels(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64, labels map[string]string) *models.AlertRule {
|
||||
ruleGroup := fmt.Sprintf("ruleGroup-%s", util.GenerateShortUID())
|
||||
err := dbstore.UpdateRuleGroup(ctx, store.UpdateRuleGroupCmd{
|
||||
OrgID: orgID,
|
||||
@@ -62,6 +66,7 @@ func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBsto
|
||||
Rules: []apimodels.PostableExtendedRuleNode{
|
||||
{
|
||||
ApiRuleNode: &apimodels.ApiRuleNode{
|
||||
Labels: labels,
|
||||
Annotations: map[string]string{"testAnnoKey": "testAnnoValue"},
|
||||
},
|
||||
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2090,7 +2090,7 @@ var expEmailNotifications = []*models.SendEmailCommandSync{
|
||||
EndsAt: time.Time{},
|
||||
GeneratorURL: "http://localhost:3000/alerting/UID_EmailAlert/edit",
|
||||
Fingerprint: "08c220aa26cd0cf5",
|
||||
SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DEmailAlert",
|
||||
SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DEmailAlert",
|
||||
DashboardURL: "",
|
||||
PanelURL: "",
|
||||
ValueString: "[ var='A' labels={} value=1 ]",
|
||||
@@ -2148,7 +2148,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
{
|
||||
"title": "[FIRING:1] SlackAlert2 ",
|
||||
"title_link": "http://localhost:3000/alerting/list",
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SlackAlert2\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SlackAlert2/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DSlackAlert2\n",
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SlackAlert2\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SlackAlert2/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSlackAlert2\n",
|
||||
"fallback": "[FIRING:1] SlackAlert2 ",
|
||||
"footer": "Grafana v",
|
||||
"footer_icon": "https://grafana.com/assets/img/fav32.png",
|
||||
@@ -2181,7 +2181,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
"component": "Integration Test",
|
||||
"group": "testgroup",
|
||||
"custom_details": {
|
||||
"firing": "\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PagerdutyAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PagerdutyAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DPagerdutyAlert\n",
|
||||
"firing": "\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PagerdutyAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PagerdutyAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DPagerdutyAlert\n",
|
||||
"num_firing": "1",
|
||||
"num_resolved": "0",
|
||||
"resolved": ""
|
||||
@@ -2201,7 +2201,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
`{
|
||||
"link": {
|
||||
"messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%3A3000%2Falerting%2Flist",
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DingDingAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DingDingAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DDingDingAlert\n",
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DingDingAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DingDingAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDingDingAlert\n",
|
||||
"title": "[FIRING:1] DingDingAlert "
|
||||
},
|
||||
"msgtype": "link"
|
||||
@@ -2226,7 +2226,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TeamsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DTeamsAlert\n",
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TeamsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTeamsAlert\n",
|
||||
"title": "Details"
|
||||
}
|
||||
],
|
||||
@@ -2252,7 +2252,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
"endsAt": "0001-01-01T00:00:00Z",
|
||||
"generatorURL": "http://localhost:3000/alerting/UID_WebhookAlert/edit",
|
||||
"fingerprint": "929467973978d053",
|
||||
"silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DWebhookAlert",
|
||||
"silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert",
|
||||
"dashboardURL": "",
|
||||
"panelURL": ""
|
||||
}
|
||||
@@ -2270,12 +2270,12 @@ var expNonEmailNotifications = map[string][]string{
|
||||
"truncatedAlerts": 0,
|
||||
"title": "[FIRING:1] WebhookAlert ",
|
||||
"state": "alerting",
|
||||
"message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = WebhookAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_WebhookAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DWebhookAlert\n"
|
||||
"message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = WebhookAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_WebhookAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert\n"
|
||||
}`,
|
||||
},
|
||||
"discord_recv/discord_test": {
|
||||
`{
|
||||
"content": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DiscordAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DiscordAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DDiscordAlert\n",
|
||||
"content": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DiscordAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DiscordAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDiscordAlert\n",
|
||||
"embeds": [
|
||||
{
|
||||
"color": 14037554,
|
||||
@@ -2303,7 +2303,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
},
|
||||
"name": "default"
|
||||
},
|
||||
"output": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SensuGoAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SensuGoAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DSensuGoAlert\n",
|
||||
"output": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SensuGoAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SensuGoAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSensuGoAlert\n",
|
||||
"status": 2
|
||||
},
|
||||
"entity": {
|
||||
@@ -2316,10 +2316,10 @@ var expNonEmailNotifications = map[string][]string{
|
||||
}`,
|
||||
},
|
||||
"pushover_recv/pushover_test": {
|
||||
"--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert \r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PushoverAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PushoverAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DPushoverAlert\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n",
|
||||
"--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert \r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PushoverAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PushoverAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DPushoverAlert\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n",
|
||||
},
|
||||
"telegram_recv/bot6sh027hs034h": {
|
||||
"--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TelegramAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TelegramAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DTelegramAlert\n\r\n--abcd--\r\n",
|
||||
"--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TelegramAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TelegramAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTelegramAlert\n\r\n--abcd--\r\n",
|
||||
},
|
||||
"googlechat_recv/googlechat_test": {
|
||||
`{
|
||||
@@ -2335,7 +2335,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
"widgets": [
|
||||
{
|
||||
"textParagraph": {
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = GoogleChatAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_GoogleChatAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DGoogleChatAlert\n"
|
||||
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = GoogleChatAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_GoogleChatAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DGoogleChatAlert\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2373,7 +2373,7 @@ var expNonEmailNotifications = map[string][]string{
|
||||
"client": "Grafana",
|
||||
"client_url": "http://localhost:3000/alerting/list",
|
||||
"description": "[FIRING:1] KafkaAlert ",
|
||||
"details": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = KafkaAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_KafkaAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DKafkaAlert\n",
|
||||
"details": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = KafkaAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_KafkaAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DKafkaAlert\n",
|
||||
"incident_key": "35c0bdb1715f9162a20d7b2a01cb2e3a4c5b1dc663571701e3f67212b696332f"
|
||||
}
|
||||
}
|
||||
@@ -2381,10 +2381,10 @@ var expNonEmailNotifications = map[string][]string{
|
||||
}`,
|
||||
},
|
||||
"line_recv/line_test": {
|
||||
`message=%5BFIRING%3A1%5D+LineAlert+%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_LineAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253DLineAlert%0A`,
|
||||
`message=%5BFIRING%3A1%5D+LineAlert+%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_LineAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DLineAlert%0A`,
|
||||
},
|
||||
"threema_recv/threema_test": {
|
||||
`from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_ThreemaAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253DThreemaAlert%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`,
|
||||
`from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_ThreemaAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DThreemaAlert%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`,
|
||||
},
|
||||
"victorops_recv/victorops_test": {
|
||||
`{
|
||||
@@ -2393,14 +2393,14 @@ var expNonEmailNotifications = map[string][]string{
|
||||
"entity_id": "633ae988fa7074bcb51f3d1c5fef2ba1c5c4ccb45b3ecbf681f7d507b078b1ae",
|
||||
"message_type": "CRITICAL",
|
||||
"monitoring_tool": "Grafana v",
|
||||
"state_message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = VictorOpsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_VictorOpsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DVictorOpsAlert\n",
|
||||
"state_message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = VictorOpsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_VictorOpsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DVictorOpsAlert\n",
|
||||
"timestamp": %s
|
||||
}`,
|
||||
},
|
||||
"opsgenie_recv/opsgenie_test": {
|
||||
`{
|
||||
"alias": "47e92f0f6ef9fe99f3954e0d6155f8d09c4b9a038d8c3105e82c0cee4c62956e",
|
||||
"description": "[FIRING:1] OpsGenieAlert \nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = OpsGenieAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_OpsGenieAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DOpsGenieAlert\n",
|
||||
"description": "[FIRING:1] OpsGenieAlert \nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = OpsGenieAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_OpsGenieAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DOpsGenieAlert\n",
|
||||
"details": {
|
||||
"url": "http://localhost:3000/alerting/list"
|
||||
},
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch"
|
||||
|
||||
cwapi "github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQueryCloudWatchMetrics(t *testing.T) {
|
||||
grafDir, cfgPath := testinfra.CreateGrafDir(t)
|
||||
|
||||
addr, sqlStore := testinfra.StartGrafana(t, grafDir, cfgPath)
|
||||
setUpDatabase(t, sqlStore)
|
||||
|
||||
origNewCWClient := cloudwatch.NewCWClient
|
||||
t.Cleanup(func() {
|
||||
cloudwatch.NewCWClient = origNewCWClient
|
||||
})
|
||||
var client cloudwatch.FakeCWClient
|
||||
cloudwatch.NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
|
||||
return client
|
||||
}
|
||||
|
||||
t.Run("Custom metrics", func(t *testing.T) {
|
||||
client = cloudwatch.FakeCWClient{
|
||||
Metrics: []*cwapi.Metric{
|
||||
{
|
||||
MetricName: aws.String("Test_MetricName"),
|
||||
Dimensions: []*cwapi.Dimension{
|
||||
{
|
||||
Name: aws.String("Test_DimensionName"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := dtos.MetricRequest{
|
||||
Queries: []*simplejson.Json{
|
||||
simplejson.NewFromAny(map[string]interface{}{
|
||||
"type": "metricFindQuery",
|
||||
"subtype": "metrics",
|
||||
"region": "us-east-1",
|
||||
"namespace": "custom",
|
||||
"datasourceId": 1,
|
||||
}),
|
||||
},
|
||||
}
|
||||
result := makeCWRequest(t, req, addr)
|
||||
|
||||
dataFrames := data.Frames{
|
||||
&data.Frame{
|
||||
RefID: "A",
|
||||
Fields: []*data.Field{
|
||||
data.NewField("text", nil, []string{"Test_MetricName"}),
|
||||
data.NewField("value", nil, []string{"Test_MetricName"}),
|
||||
},
|
||||
Meta: &data.FrameMeta{
|
||||
Custom: map[string]interface{}{
|
||||
"rowCount": float64(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect := backend.NewQueryDataResponse()
|
||||
expect.Responses["A"] = backend.DataResponse{
|
||||
Frames: dataFrames,
|
||||
}
|
||||
assert.Equal(t, *expect, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryCloudWatchLogs(t *testing.T) {
|
||||
grafDir, cfgPath := testinfra.CreateGrafDir(t)
|
||||
addr, store := testinfra.StartGrafana(t, grafDir, cfgPath)
|
||||
setUpDatabase(t, store)
|
||||
|
||||
origNewCWLogsClient := cloudwatch.NewCWLogsClient
|
||||
t.Cleanup(func() {
|
||||
cloudwatch.NewCWLogsClient = origNewCWLogsClient
|
||||
})
|
||||
|
||||
var client cloudwatch.FakeCWLogsClient
|
||||
cloudwatch.NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return client
|
||||
}
|
||||
|
||||
t.Run("Describe log groups", func(t *testing.T) {
|
||||
client = cloudwatch.FakeCWLogsClient{}
|
||||
|
||||
req := dtos.MetricRequest{
|
||||
Queries: []*simplejson.Json{
|
||||
simplejson.NewFromAny(map[string]interface{}{
|
||||
"type": "logAction",
|
||||
"subtype": "DescribeLogGroups",
|
||||
"region": "us-east-1",
|
||||
"datasourceId": 1,
|
||||
}),
|
||||
},
|
||||
}
|
||||
tr := makeCWRequest(t, req, addr)
|
||||
|
||||
dataFrames := data.Frames{
|
||||
&data.Frame{
|
||||
Name: "logGroups",
|
||||
RefID: "A",
|
||||
Fields: []*data.Field{
|
||||
data.NewField("logGroupName", nil, []*string{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect := backend.NewQueryDataResponse()
|
||||
expect.Responses["A"] = backend.DataResponse{
|
||||
Frames: dataFrames,
|
||||
}
|
||||
assert.Equal(t, *expect, tr)
|
||||
})
|
||||
}
|
||||
|
||||
func makeCWRequest(t *testing.T, req dtos.MetricRequest, addr string) backend.QueryDataResponse {
|
||||
t.Helper()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
enc := json.NewEncoder(&buf)
|
||||
err := enc.Encode(&req)
|
||||
require.NoError(t, err)
|
||||
u := fmt.Sprintf("http://%s/api/ds/query", addr)
|
||||
t.Logf("Making POST request to %s", u)
|
||||
// nolint:gosec
|
||||
resp, err := http.Post(u, "application/json", &buf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
t.Cleanup(func() {
|
||||
err := resp.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
buf = bytes.Buffer{}
|
||||
_, err = io.Copy(&buf, resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var tr backend.QueryDataResponse
|
||||
err = json.Unmarshal(buf.Bytes(), &tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
func setUpDatabase(t *testing.T, store *sqlstore.SQLStore) {
|
||||
t.Helper()
|
||||
|
||||
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
_, err := sess.Insert(&models.DataSource{
|
||||
Id: 1,
|
||||
// This will be the ID of the main org
|
||||
OrgId: 2,
|
||||
Name: "Test",
|
||||
Type: "cloudwatch",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
})
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make sure changes are synced with other goroutines
|
||||
err = store.Sync()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -97,10 +97,3 @@ func TestQuery_AnnotationQuery(t *testing.T) {
|
||||
}, client.calls.describeAlarms[0])
|
||||
})
|
||||
}
|
||||
|
||||
func pointerString(s string) *string {
|
||||
return &s
|
||||
}
|
||||
func pointerInt64(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var counter = 1
|
||||
|
||||
type cloudWatchFakeClient struct {
|
||||
cloudwatchiface.CloudWatchAPI
|
||||
|
||||
counterForGetMetricDataWithContext int
|
||||
}
|
||||
|
||||
func (client *cloudWatchFakeClient) GetMetricDataWithContext(ctx aws.Context, input *cloudwatch.GetMetricDataInput, opts ...request.Option) (*cloudwatch.GetMetricDataOutput, error) {
|
||||
@@ -23,13 +23,13 @@ func (client *cloudWatchFakeClient) GetMetricDataWithContext(ctx aws.Context, in
|
||||
res := []*cloudwatch.MetricDataResult{{
|
||||
Values: []*float64{aws.Float64(12.3), aws.Float64(23.5)},
|
||||
}}
|
||||
if counter == 0 {
|
||||
if client.counterForGetMetricDataWithContext == 0 {
|
||||
nextToken = ""
|
||||
res = []*cloudwatch.MetricDataResult{{
|
||||
Values: []*float64{aws.Float64(100)},
|
||||
}}
|
||||
}
|
||||
counter--
|
||||
client.counterForGetMetricDataWithContext--
|
||||
return &cloudwatch.GetMetricDataOutput{
|
||||
MetricDataResults: res,
|
||||
NextToken: aws.String(nextToken),
|
||||
@@ -39,7 +39,7 @@ func (client *cloudWatchFakeClient) GetMetricDataWithContext(ctx aws.Context, in
|
||||
func TestGetMetricDataExecutorTest(t *testing.T) {
|
||||
executor := &cloudWatchExecutor{}
|
||||
inputs := &cloudwatch.GetMetricDataInput{MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
|
||||
res, err := executor.executeRequest(context.Background(), &cloudWatchFakeClient{}, inputs)
|
||||
res, err := executor.executeRequest(context.Background(), &cloudWatchFakeClient{counterForGetMetricDataWithContext: 1}, inputs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 2)
|
||||
require.Len(t, res[0].MetricDataResults[0].Values, 2)
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestQuery_DescribeLogGroups(t *testing.T) {
|
||||
var cli FakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return cli
|
||||
return &cli
|
||||
}
|
||||
|
||||
t.Run("Empty log group name prefix", func(t *testing.T) {
|
||||
@@ -155,7 +155,7 @@ func TestQuery_GetLogGroupFields(t *testing.T) {
|
||||
var cli FakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return cli
|
||||
return &cli
|
||||
}
|
||||
|
||||
cli = FakeCWLogsClient{
|
||||
@@ -232,7 +232,7 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
var cli FakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return cli
|
||||
return &cli
|
||||
}
|
||||
|
||||
t.Run("invalid time range", func(t *testing.T) {
|
||||
@@ -357,6 +357,108 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_executeStartQuery(t *testing.T) {
|
||||
origNewCWLogsClient := NewCWLogsClient
|
||||
t.Cleanup(func() {
|
||||
NewCWLogsClient = origNewCWLogsClient
|
||||
})
|
||||
|
||||
var cli FakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return &cli
|
||||
}
|
||||
|
||||
t.Run("successfully parses information from JSON to StartQueryWithContext", func(t *testing.T) {
|
||||
cli = FakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return datasourceInfo{}, nil
|
||||
})
|
||||
executor := newExecutor(im, newTestConfig(), fakeSessionCache{})
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
|
||||
JSON: json.RawMessage(`{
|
||||
"type": "logAction",
|
||||
"subtype": "StartQuery",
|
||||
"limit": 12,
|
||||
"queryString":"fields @message",
|
||||
"logGroupNames":["some name","another name"]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*cloudwatchlogs.StartQueryInput{
|
||||
{
|
||||
StartTime: pointerInt64(0),
|
||||
EndTime: pointerInt64(1),
|
||||
Limit: pointerInt64(12),
|
||||
QueryString: pointerString("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []*string{pointerString("some name"), pointerString("another name")},
|
||||
},
|
||||
}, cli.calls.startQueryWithContext)
|
||||
})
|
||||
|
||||
t.Run("cannot parse limit as float", func(t *testing.T) {
|
||||
cli = FakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return datasourceInfo{}, nil
|
||||
})
|
||||
executor := newExecutor(im, newTestConfig(), fakeSessionCache{})
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
|
||||
JSON: json.RawMessage(`{
|
||||
"type": "logAction",
|
||||
"subtype": "StartQuery",
|
||||
"limit": 12.0
|
||||
}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, cli.calls.startQueryWithContext, 1)
|
||||
assert.Nil(t, cli.calls.startQueryWithContext[0].Limit)
|
||||
})
|
||||
|
||||
t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) {
|
||||
cli = FakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return datasourceInfo{}, nil
|
||||
})
|
||||
executor := newExecutor(im, newTestConfig(), fakeSessionCache{})
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
|
||||
JSON: json.RawMessage(`{
|
||||
"type": "logAction",
|
||||
"subtype": "StartQuery"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, cli.calls.startQueryWithContext, 1)
|
||||
assert.Nil(t, cli.calls.startQueryWithContext[0].Limit)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_StopQuery(t *testing.T) {
|
||||
origNewCWLogsClient := NewCWLogsClient
|
||||
t.Cleanup(func() {
|
||||
@@ -366,7 +468,7 @@ func TestQuery_StopQuery(t *testing.T) {
|
||||
var cli FakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return cli
|
||||
return &cli
|
||||
}
|
||||
|
||||
cli = FakeCWLogsClient{
|
||||
@@ -438,7 +540,7 @@ func TestQuery_GetQueryResults(t *testing.T) {
|
||||
var cli FakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return cli
|
||||
return &cli
|
||||
}
|
||||
|
||||
const refID = "A"
|
||||
|
||||
@@ -514,7 +514,7 @@ var dimensionsMap = map[string][]string{
|
||||
// Known AWS regions.
|
||||
var knownRegions = []string{
|
||||
"af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1",
|
||||
"ap-southeast-2", "ca-central-1", "cn-north-1", "cn-northwest-1", "eu-central-1", "eu-north-1", "eu-south-1", "eu-west-1",
|
||||
"ap-southeast-2", "ap-southeast-3", "ca-central-1", "cn-north-1", "cn-northwest-1", "eu-central-1", "eu-north-1", "eu-south-1", "eu-west-1",
|
||||
"eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-gov-east-1", "us-gov-west-1",
|
||||
"us-iso-east-1", "us-isob-east-1", "us-west-1", "us-west-2",
|
||||
}
|
||||
|
||||
@@ -20,32 +20,41 @@ import (
|
||||
|
||||
type FakeCWLogsClient struct {
|
||||
cloudwatchlogsiface.CloudWatchLogsAPI
|
||||
|
||||
calls logsQueryCalls
|
||||
|
||||
logGroups cloudwatchlogs.DescribeLogGroupsOutput
|
||||
logGroupFields cloudwatchlogs.GetLogGroupFieldsOutput
|
||||
queryResults cloudwatchlogs.GetQueryResultsOutput
|
||||
}
|
||||
|
||||
func (m FakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
type logsQueryCalls struct {
|
||||
startQueryWithContext []*cloudwatchlogs.StartQueryInput
|
||||
}
|
||||
|
||||
func (m *FakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
return &m.queryResults, nil
|
||||
}
|
||||
|
||||
func (m FakeCWLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||
func (m *FakeCWLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||
m.calls.startQueryWithContext = append(m.calls.startQueryWithContext, input)
|
||||
|
||||
return &cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m FakeCWLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||
func (m *FakeCWLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||
return &cloudwatchlogs.StopQueryOutput{
|
||||
Success: aws.Bool(true),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m FakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
func (m *FakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
return &m.logGroups, nil
|
||||
}
|
||||
|
||||
func (m FakeCWLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
|
||||
func (m *FakeCWLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
|
||||
return &m.logGroupFields, nil
|
||||
}
|
||||
|
||||
@@ -192,3 +201,10 @@ func (s fakeSessionCache) GetSession(c awsds.SessionConfig) (*session.Session, e
|
||||
Config: &aws.Config{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func pointerString(s string) *string {
|
||||
return &s
|
||||
}
|
||||
func pointerInt64(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer';
|
||||
import { CustomVariableModel } from 'app/features/variables/types';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
export function setupMockedDataSource({ data = [], variables }: { data?: any; variables?: any } = {}) {
|
||||
let templateService = new TemplateSrvMock({
|
||||
@@ -16,6 +17,8 @@ export function setupMockedDataSource({ data = [], variables }: { data?: any; va
|
||||
if (variables) {
|
||||
templateService = new TemplateSrv();
|
||||
templateService.init(variables);
|
||||
templateService.getVariables = jest.fn().mockReturnValue(variables);
|
||||
templateService.getVariableName = (name: string) => name;
|
||||
}
|
||||
|
||||
const datasource = new CloudWatchDatasource(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { ExploreMode, SelectableValue } from '@grafana/data';
|
||||
import { EditorHeader, InlineSelect } from '@grafana/experimental';
|
||||
import { pick } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { CloudWatchQuery, CloudWatchQueryMode } from '../types';
|
||||
import { useRegions } from '../hooks';
|
||||
import { CloudWatchQuery, CloudWatchQueryMode } from '../types';
|
||||
import MetricsQueryHeader from './MetricsQueryHeader';
|
||||
|
||||
interface QueryHeaderProps {
|
||||
@@ -59,7 +58,7 @@ const QueryHeader: React.FC<QueryHeaderProps> = ({
|
||||
<EditorHeader>
|
||||
<InlineSelect
|
||||
label="Region"
|
||||
value={regions.find((v) => v.value === region)}
|
||||
value={region}
|
||||
placeholder="Select region"
|
||||
allowCustomValue
|
||||
onChange={({ value: region }) => region && onRegion({ value: region })}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { lastValueFrom, of } from 'rxjs';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { ArrayVector, DataFrame, dataFrameToJSON, dateTime, Field, MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { lastValueFrom, of } from 'rxjs';
|
||||
import { toArray } from 'rxjs/operators';
|
||||
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType, CloudWatchLogsQueryStatus } from './types';
|
||||
|
||||
import {
|
||||
setupMockedDataSource,
|
||||
namespaceVariable,
|
||||
metricVariable,
|
||||
labelsVariable,
|
||||
limitVariable,
|
||||
metricVariable,
|
||||
namespaceVariable,
|
||||
setupMockedDataSource,
|
||||
} from './__mocks__/CloudWatchDataSource';
|
||||
import { CloudWatchDatasource } from './datasource';
|
||||
import { CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from './types';
|
||||
|
||||
describe('datasource', () => {
|
||||
describe('query', () => {
|
||||
@@ -91,6 +91,57 @@ describe('datasource', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('debouncedCustomAlert', () => {
|
||||
const debouncedAlert = jest.fn();
|
||||
beforeEach(() => {
|
||||
const { datasource } = setupMockedDataSource({
|
||||
variables: [
|
||||
{ ...namespaceVariable, multi: true },
|
||||
{ ...metricVariable, multi: true },
|
||||
],
|
||||
});
|
||||
datasource.debouncedCustomAlert = debouncedAlert;
|
||||
datasource.performTimeSeriesQuery = jest.fn().mockResolvedValue([]);
|
||||
datasource.query({
|
||||
targets: [
|
||||
{
|
||||
queryMode: 'Metrics',
|
||||
id: '',
|
||||
region: 'us-east-2',
|
||||
namespace: namespaceVariable.id,
|
||||
metricName: metricVariable.id,
|
||||
period: '',
|
||||
alias: '',
|
||||
dimensions: {},
|
||||
matchExact: true,
|
||||
statistic: '',
|
||||
refId: '',
|
||||
expression: 'x * 2',
|
||||
metricQueryType: MetricQueryType.Search,
|
||||
metricEditorMode: MetricEditorMode.Code,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
});
|
||||
it('should show debounced alert for namespace and metric name', async () => {
|
||||
expect(debouncedAlert).toHaveBeenCalledWith(
|
||||
'CloudWatch templating error',
|
||||
'Multi template variables are not supported for namespace'
|
||||
);
|
||||
expect(debouncedAlert).toHaveBeenCalledWith(
|
||||
'CloudWatch templating error',
|
||||
'Multi template variables are not supported for metric name'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show debounced alert for region', async () => {
|
||||
expect(debouncedAlert).not.toHaveBeenCalledWith(
|
||||
'CloudWatch templating error',
|
||||
'Multi template variables are not supported for region'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMetricQuery', () => {
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import React from 'react';
|
||||
import { cloneDeep, find, findLast, isEmpty, isString, set } from 'lodash';
|
||||
import { from, lastValueFrom, merge, Observable, of, throwError, zip } from 'rxjs';
|
||||
import { catchError, concatMap, finalize, map, mergeMap, repeat, scan, share, takeWhile, tap } from 'rxjs/operators';
|
||||
import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||
import {
|
||||
DataFrame,
|
||||
DataQueryError,
|
||||
@@ -22,45 +16,51 @@ import {
|
||||
TimeRange,
|
||||
toLegacyResponseData,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||
import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
|
||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { AppNotificationTimeout } from 'app/types';
|
||||
import { store } from 'app/store/store';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { VariableWithMultiSupport } from 'app/features/variables/types';
|
||||
import { store } from 'app/store/store';
|
||||
import { AppNotificationTimeout } from 'app/types';
|
||||
import { cloneDeep, find, findLast, isEmpty, isString, set } from 'lodash';
|
||||
import React from 'react';
|
||||
import { from, lastValueFrom, merge, Observable, of, throwError, zip } from 'rxjs';
|
||||
import { catchError, concatMap, finalize, map, mergeMap, repeat, scan, share, takeWhile, tap } from 'rxjs/operators';
|
||||
|
||||
import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider';
|
||||
import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage';
|
||||
import { CloudWatchLanguageProvider } from './language_provider';
|
||||
import memoizedDebounce from './memoizedDebounce';
|
||||
import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider';
|
||||
import {
|
||||
MetricEditorMode,
|
||||
CloudWatchJsonData,
|
||||
CloudWatchLogsQuery,
|
||||
CloudWatchLogsQueryStatus,
|
||||
CloudWatchLogsRequest,
|
||||
CloudWatchMetricsQuery,
|
||||
CloudWatchQuery,
|
||||
DescribeLogGroupsRequest,
|
||||
Dimensions,
|
||||
GetLogEventsRequest,
|
||||
GetLogGroupFieldsRequest,
|
||||
GetLogGroupFieldsResponse,
|
||||
isCloudWatchLogsQuery,
|
||||
LogAction,
|
||||
MetricQueryType,
|
||||
MetricEditorMode,
|
||||
MetricFindSuggestData,
|
||||
MetricQuery,
|
||||
MetricQueryType,
|
||||
MetricRequest,
|
||||
StartQueryRequest,
|
||||
TSDBResponse,
|
||||
Dimensions,
|
||||
MetricFindSuggestData,
|
||||
CloudWatchLogsRequest,
|
||||
} from './types';
|
||||
import { CloudWatchLanguageProvider } from './language_provider';
|
||||
import { VariableWithMultiSupport } from 'app/features/variables/types';
|
||||
import { increasingInterval } from './utils/rxjs/increasingInterval';
|
||||
import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
|
||||
import { addDataLinksToLogsResponse } from './utils/datalinks';
|
||||
import { runWithRetry } from './utils/logsRetry';
|
||||
import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider';
|
||||
import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider';
|
||||
import { increasingInterval } from './utils/rxjs/increasingInterval';
|
||||
|
||||
const DS_QUERY_ENDPOINT = '/api/ds/query';
|
||||
|
||||
@@ -267,7 +267,7 @@ export class CloudWatchDatasource
|
||||
const validMetricsQueries = metricQueries
|
||||
.filter(this.filterMetricQuery)
|
||||
.map((item: CloudWatchMetricsQuery): MetricQuery => {
|
||||
item.region = this.replace(this.getActualRegion(item.region), options.scopedVars, true, 'region');
|
||||
item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
|
||||
item.namespace = this.replace(item.namespace, options.scopedVars, true, 'namespace');
|
||||
item.metricName = this.replace(item.metricName, options.scopedVars, true, 'metric name');
|
||||
item.dimensions = this.convertDimensionFormat(item.dimensions ?? {}, options.scopedVars);
|
||||
|
||||
+20
-7
@@ -1,11 +1,12 @@
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource';
|
||||
import FakeSchemaData from './__mocks__/schema';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types';
|
||||
import { toUtc } from '@grafana/data';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import createMockQuery from '../__mocks__/query';
|
||||
import { singleVariable } from '../__mocks__/variables';
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types';
|
||||
import FakeSchemaData from './__mocks__/schema';
|
||||
import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource';
|
||||
|
||||
const templateSrv = new TemplateSrv();
|
||||
|
||||
@@ -273,7 +274,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
laDatasource = new AzureLogAnalyticsDatasource(ctx.instanceSettings);
|
||||
});
|
||||
|
||||
it('should run complete queries', () => {
|
||||
it('should run queries with a resource', () => {
|
||||
const query: AzureMonitorQuery = {
|
||||
refId: 'A',
|
||||
azureLogAnalytics: {
|
||||
@@ -285,6 +286,18 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
expect(laDatasource.filterQuery(query)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should run queries with a workspace', () => {
|
||||
const query: AzureMonitorQuery = {
|
||||
refId: 'A',
|
||||
azureLogAnalytics: {
|
||||
query: 'perf | take 100',
|
||||
workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
};
|
||||
|
||||
expect(laDatasource.filterQuery(query)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not run empty queries', () => {
|
||||
const query: AzureMonitorQuery = {
|
||||
refId: 'A',
|
||||
@@ -317,7 +330,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
expect(laDatasource.filterQuery(query)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not run queries missing a resource', () => {
|
||||
it('should not run queries missing a resource and a missing workspace', () => {
|
||||
const query: AzureMonitorQuery = {
|
||||
refId: 'A',
|
||||
azureLogAnalytics: {
|
||||
|
||||
+20
-15
@@ -1,26 +1,27 @@
|
||||
import { map } from 'lodash';
|
||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
||||
import ResponseParser, { transformMetadataToKustoSchema } from './response_parser';
|
||||
import {
|
||||
AzureMonitorQuery,
|
||||
AzureDataSourceJsonData,
|
||||
AzureLogsVariable,
|
||||
AzureQueryType,
|
||||
DatasourceValidationResult,
|
||||
} from '../types';
|
||||
import {
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
ScopedVars,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceRef,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
|
||||
import { map } from 'lodash';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
||||
|
||||
import { isGUIDish } from '../components/ResourcePicker/utils';
|
||||
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
||||
import {
|
||||
AzureDataSourceJsonData,
|
||||
AzureLogsVariable,
|
||||
AzureMonitorQuery,
|
||||
AzureQueryType,
|
||||
DatasourceValidationResult,
|
||||
} from '../types';
|
||||
import { interpolateVariable, routeNames } from '../utils/common';
|
||||
import ResponseParser, { transformMetadataToKustoSchema } from './response_parser';
|
||||
|
||||
interface AdhocQuery {
|
||||
datasource: DataSourceRef;
|
||||
@@ -60,7 +61,11 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
filterQuery(item: AzureMonitorQuery): boolean {
|
||||
return item.hide !== true && !!item.azureLogAnalytics?.query && !!item.azureLogAnalytics.resource;
|
||||
return (
|
||||
item.hide !== true &&
|
||||
!!item.azureLogAnalytics?.query &&
|
||||
(!!item.azureLogAnalytics.resource || !!item.azureLogAnalytics.workspace)
|
||||
);
|
||||
}
|
||||
|
||||
async getSubscriptions(): Promise<Array<{ text: string; value: string }>> {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
import { DataQueryRequest, DataSourceInstanceSettings, dateTime, PluginType } from '@grafana/data';
|
||||
import { of } from 'rxjs';
|
||||
import { JaegerDatasource, JaegerJsonData } from '../datasource';
|
||||
import { JaegerQuery } from '../types';
|
||||
import React from 'react';
|
||||
import SearchForm from './SearchForm';
|
||||
import { testResponse } from '../testResponse';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('SearchForm', () => {
|
||||
it('should call the `onChange` function on click of the Input', async () => {
|
||||
const promise = Promise.resolve();
|
||||
const handleOnChange = jest.fn(() => promise);
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
refId: '121314',
|
||||
};
|
||||
const ds = {
|
||||
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> {
|
||||
if (url === '/api/services') {
|
||||
return Promise.resolve(['jaeger-query', 'service2', 'service3']);
|
||||
}
|
||||
},
|
||||
} as JaegerDatasource;
|
||||
setupFetchMock({ data: [testResponse] });
|
||||
|
||||
render(<SearchForm datasource={ds} query={query} onChange={handleOnChange} />);
|
||||
|
||||
const asyncServiceSelect = await waitFor(() => screen.getByRole('combobox', { name: 'select-service-name' }));
|
||||
expect(asyncServiceSelect).toBeInTheDocument();
|
||||
|
||||
userEvent.click(asyncServiceSelect);
|
||||
|
||||
const jaegerService = await screen.findByText('jaeger-query');
|
||||
expect(jaegerService).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to select operation name if query.service exists', async () => {
|
||||
const promise = Promise.resolve();
|
||||
const handleOnChange = jest.fn(() => promise);
|
||||
const query2 = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
refId: '121314',
|
||||
service: 'jaeger-query',
|
||||
};
|
||||
setupFetchMock({ data: [testResponse] });
|
||||
|
||||
render(<SearchForm datasource={{} as JaegerDatasource} query={query2} onChange={handleOnChange} />);
|
||||
|
||||
const asyncOperationSelect2 = await waitFor(() => screen.getByRole('combobox', { name: 'select-operation-name' }));
|
||||
expect(asyncOperationSelect2).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchForm', () => {
|
||||
it('should show loader if there is a delay fetching options', async () => {
|
||||
const promise = Promise.resolve();
|
||||
const handleOnChange = jest.fn(() => {
|
||||
setTimeout(() => {
|
||||
return promise;
|
||||
}, 3000);
|
||||
});
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
refId: '121314',
|
||||
service: 'jaeger-query',
|
||||
};
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
setupFetchMock({ data: [testResponse] });
|
||||
|
||||
render(<SearchForm datasource={ds} query={query} onChange={handleOnChange} />);
|
||||
|
||||
const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' });
|
||||
userEvent.click(asyncServiceSelect);
|
||||
const loader = screen.getByText('Loading options...');
|
||||
|
||||
expect(loader).toBeInTheDocument();
|
||||
await act(() => promise);
|
||||
});
|
||||
});
|
||||
|
||||
function setupFetchMock(response: any, mock?: any) {
|
||||
const defaultMock = () => mock ?? of(createFetchResponse(response));
|
||||
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
fetchMock.mockImplementation(defaultMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = {
|
||||
id: 0,
|
||||
uid: '0',
|
||||
type: 'tracing',
|
||||
name: 'jaeger',
|
||||
url: 'http://grafana.com',
|
||||
access: 'proxy',
|
||||
meta: {
|
||||
id: 'jaeger',
|
||||
name: 'jaeger',
|
||||
type: PluginType.datasource,
|
||||
info: {} as any,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
},
|
||||
jsonData: {
|
||||
nodeGraph: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultQuery: DataQueryRequest<JaegerQuery> = {
|
||||
requestId: '1',
|
||||
dashboardId: 0,
|
||||
interval: '0',
|
||||
intervalMs: 10,
|
||||
panelId: 0,
|
||||
scopedVars: {},
|
||||
range: {
|
||||
from: dateTime().subtract(1, 'h'),
|
||||
to: dateTime(),
|
||||
raw: { from: '1h', to: 'now' },
|
||||
},
|
||||
timezone: 'browser',
|
||||
app: 'explore',
|
||||
startTime: 0,
|
||||
targets: [
|
||||
{
|
||||
query: '12345',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AsyncSelect, InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { JaegerDatasource } from '../datasource';
|
||||
import { JaegerQuery } from '../types';
|
||||
import { transformToLogfmt } from '../util';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
|
||||
type Props = {
|
||||
datasource: JaegerDatasource;
|
||||
@@ -22,69 +25,110 @@ const allOperationsOption: SelectableValue<string> = {
|
||||
export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [operationOptions, setOperationOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [isLoading, setIsLoading] = useState<{
|
||||
services: boolean;
|
||||
operations: boolean;
|
||||
}>({
|
||||
services: false,
|
||||
operations: false,
|
||||
});
|
||||
|
||||
const loadServices = useCallback(
|
||||
async (url: string, loaderOfType: string): Promise<Array<SelectableValue<string>>> => {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: true }));
|
||||
|
||||
try {
|
||||
const values: string[] | null = await datasource.metadataRequest(url);
|
||||
if (!values) {
|
||||
return [{ label: `No ${loaderOfType} found`, value: `No ${loaderOfType} found` }];
|
||||
}
|
||||
|
||||
const serviceOptions: SelectableValue[] = values.sort().map((service) => ({
|
||||
label: service,
|
||||
value: service,
|
||||
}));
|
||||
return serviceOptions;
|
||||
} catch (error) {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
return [];
|
||||
} finally {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false }));
|
||||
}
|
||||
},
|
||||
[datasource]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const getServices = async () => {
|
||||
const services = await loadServices({
|
||||
dataSource: datasource,
|
||||
url: '/api/services',
|
||||
notFoundLabel: 'No service found',
|
||||
});
|
||||
const services = await loadServices('/api/services', 'services');
|
||||
setServiceOptions(services);
|
||||
};
|
||||
getServices();
|
||||
}, [datasource]);
|
||||
}, [datasource, loadServices]);
|
||||
|
||||
useEffect(() => {
|
||||
const getOperations = async () => {
|
||||
const operations = await loadServices({
|
||||
dataSource: datasource,
|
||||
url: `/api/services/${encodeURIComponent(query.service!)}/operations`,
|
||||
notFoundLabel: 'No operation found',
|
||||
});
|
||||
const operations = await loadServices(
|
||||
`/api/services/${encodeURIComponent(query.service!)}/operations`,
|
||||
'operations'
|
||||
);
|
||||
setOperationOptions([allOperationsOption, ...operations]);
|
||||
};
|
||||
if (query.service) {
|
||||
getOperations();
|
||||
}
|
||||
}, [datasource, query.service]);
|
||||
}, [datasource, query.service, loadServices]);
|
||||
|
||||
return (
|
||||
<div className={css({ maxWidth: '500px' })}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Service" labelWidth={14} grow>
|
||||
<Select
|
||||
<AsyncSelect
|
||||
inputId="service"
|
||||
menuShouldPortal
|
||||
options={serviceOptions}
|
||||
value={serviceOptions?.find((v) => v.value === query.service) || null}
|
||||
onChange={(v) => {
|
||||
cacheOptions={false}
|
||||
loadOptions={() => loadServices('/api/services', 'services')}
|
||||
onOpenMenu={() => loadServices('/api/services', 'services')}
|
||||
isLoading={isLoading.services}
|
||||
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
service: v.value!,
|
||||
operation: query.service !== v.value ? undefined : query.operation,
|
||||
});
|
||||
}}
|
||||
service: v?.value!,
|
||||
operation: query.service !== v?.value ? undefined : query.operation,
|
||||
})
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
defaultOptions
|
||||
aria-label={'select-service-name'}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Operation" labelWidth={14} grow disabled={!query.service}>
|
||||
<Select
|
||||
<AsyncSelect
|
||||
inputId="operation"
|
||||
menuShouldPortal
|
||||
options={operationOptions}
|
||||
cacheOptions={false}
|
||||
loadOptions={() =>
|
||||
loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations')
|
||||
}
|
||||
onOpenMenu={() =>
|
||||
loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations')
|
||||
}
|
||||
isLoading={isLoading.operations}
|
||||
value={operationOptions?.find((v) => v.value === query.operation) || null}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
operation: v.value!,
|
||||
operation: v?.value! || undefined,
|
||||
})
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
defaultOptions
|
||||
aria-label={'select-operation-name'}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
@@ -108,19 +152,4 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
type Options = { dataSource: JaegerDatasource; url: string; notFoundLabel: string };
|
||||
|
||||
const loadServices = async ({ dataSource, url, notFoundLabel }: Options): Promise<Array<SelectableValue<string>>> => {
|
||||
const services: string[] | null = await dataSource.metadataRequest(url);
|
||||
|
||||
if (!services) {
|
||||
return [{ label: notFoundLabel, value: notFoundLabel }];
|
||||
}
|
||||
|
||||
const serviceOptions: SelectableValue[] = services.sort().map((service) => ({
|
||||
label: service,
|
||||
value: service,
|
||||
}));
|
||||
|
||||
return serviceOptions;
|
||||
};
|
||||
export default SearchForm;
|
||||
|
||||
@@ -138,7 +138,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
// this expands the distr: 2 scale so that the indicies of each data[0] land at the proper justified positions
|
||||
const xRange: Scale.Range = (u, min, max) => {
|
||||
min = 0;
|
||||
max = u.data[0].length - 1;
|
||||
max = Math.max(1, u.data[0].length - 1);
|
||||
|
||||
let pctOffset = 0;
|
||||
|
||||
@@ -148,13 +148,17 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
});
|
||||
|
||||
// expand scale range by equal amounts on both ends
|
||||
let rn = max - min; // TODO: clamp to 1?
|
||||
let rn = max - min;
|
||||
|
||||
let upScale = 1 / (1 - pctOffset * 2);
|
||||
let offset = (upScale * rn - rn) / 2;
|
||||
if (pctOffset === 0.5) {
|
||||
min -= rn;
|
||||
} else {
|
||||
let upScale = 1 / (1 - pctOffset * 2);
|
||||
let offset = (upScale * rn - rn) / 2;
|
||||
|
||||
min -= offset;
|
||||
max += offset;
|
||||
min -= offset;
|
||||
max += offset;
|
||||
}
|
||||
|
||||
return [min, max];
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import config from 'app/core/config';
|
||||
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
|
||||
|
||||
type Options = {
|
||||
dataList: DataFrame[];
|
||||
@@ -30,13 +31,15 @@ export class DataProcessor {
|
||||
}
|
||||
|
||||
for (let i = 0; i < dataList.length; i++) {
|
||||
const series = dataList[i];
|
||||
let series = dataList[i];
|
||||
const { timeField } = getTimeField(series);
|
||||
|
||||
if (!timeField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
series = applyNullInsertThreshold(series, timeField.name);
|
||||
|
||||
for (let j = 0; j < series.fields.length; j++) {
|
||||
const field = series.fields[j];
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@ import {
|
||||
getFieldSeriesColor,
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
import { Themeable2, UPlotConfigBuilder, UPlotChart, VizLayout, PlotLegend } from '@grafana/ui';
|
||||
import {
|
||||
Themeable2,
|
||||
UPlotConfigBuilder,
|
||||
UPlotChart,
|
||||
VizLayout,
|
||||
PlotLegend,
|
||||
measureText,
|
||||
UPLOT_AXIS_FONT_SIZE,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import {
|
||||
histogramBucketSizes,
|
||||
@@ -119,7 +127,20 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
|
||||
placement: AxisPlacement.Bottom,
|
||||
incrs: histogramBucketSizes,
|
||||
splits: xSplits,
|
||||
values: (u: uPlot, vals: any[]) => vals.map(xAxisFormatter),
|
||||
values: (u: uPlot, splits: any[]) => {
|
||||
const tickLabels = splits.map(xAxisFormatter);
|
||||
|
||||
const maxWidth = tickLabels.reduce(
|
||||
(curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax),
|
||||
0
|
||||
);
|
||||
|
||||
const labelSpacing = 10;
|
||||
const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio);
|
||||
const keepMod = Math.ceil(tickLabels.length / maxCount);
|
||||
|
||||
return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null));
|
||||
},
|
||||
//incrs: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mult) => mult * bucketSize),
|
||||
//splits: config.xSplits,
|
||||
//values: config.xValues,
|
||||
|
||||
@@ -553,16 +553,18 @@ export function findNextStateIndex(field: Field, datapointIdx: number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startValue = field.values.get(datapointIdx);
|
||||
|
||||
while (end === undefined) {
|
||||
if (rightPointer >= field.values.length) {
|
||||
return null;
|
||||
}
|
||||
const rightValue = field.values.get(rightPointer);
|
||||
|
||||
if (rightValue !== undefined) {
|
||||
end = rightPointer;
|
||||
} else {
|
||||
if (rightValue === undefined || rightValue === startValue) {
|
||||
rightPointer++;
|
||||
} else {
|
||||
end = rightPointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user