Merge branch 'main' into encryption/use-secrets-service
This commit is contained in:
@@ -15,4 +15,11 @@ node_modules
|
||||
/local
|
||||
/tmp
|
||||
*.yml
|
||||
!.yarnrc.yml
|
||||
*.md
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/versions
|
||||
!.yarn/cache
|
||||
|
||||
@@ -63,6 +63,7 @@ go.sum @grafana/backend-platform
|
||||
/pkg/plugins @grafana/plugins-platform-backend
|
||||
/pkg/services/datasourceproxy @grafana/plugins-platform-backend
|
||||
/pkg/services/datasources @grafana/plugins-platform-backend
|
||||
/public/app/features/plugins @grafana/plugins-platform-frontend
|
||||
|
||||
# Backend code docs
|
||||
/contribute/style-guides/backend.md @grafana/backend-platform
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
],
|
||||
"enabledManagers": ["npm"],
|
||||
"ignoreDeps": [
|
||||
"@types/systemjs",
|
||||
"@types/d3-force",
|
||||
"d3",
|
||||
"d3-force", // we should bump this once we move to esm modules
|
||||
"husky",
|
||||
"slate",
|
||||
"slate-plain-serializer",
|
||||
@@ -22,7 +25,7 @@
|
||||
"matchPaths": ["grafana-toolkit/package.json"],
|
||||
"ignoreDeps": [
|
||||
"copy-webpack-plugin", // need to wait for Grafana 9 to upgrade toolkit to webpack 5
|
||||
"css-loader", // need to wait for Grafana 9 to upgrade toolkit to webpack 5
|
||||
"css-loader", // need to wait for Grafana 9 to upgrade toolkit to webpack 5
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
+8
-5
@@ -3,10 +3,12 @@ FROM node:16-alpine3.14 as js-builder
|
||||
WORKDIR /usr/src/app/
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
COPY .yarnrc.yml ./
|
||||
COPY .yarn .yarn
|
||||
COPY packages packages
|
||||
COPY plugins-bundled plugins-bundled
|
||||
|
||||
RUN apk --no-cache add git
|
||||
RUN yarn install --pure-lockfile --no-progress
|
||||
RUN yarn install
|
||||
|
||||
COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./
|
||||
COPY public public
|
||||
@@ -19,7 +21,7 @@ RUN yarn build
|
||||
|
||||
FROM golang:1.17.0-alpine3.14 as go-builder
|
||||
|
||||
RUN apk add --no-cache gcc g++
|
||||
RUN apk add --no-cache gcc g++ make
|
||||
|
||||
WORKDIR $GOPATH/src/github.com/grafana/grafana
|
||||
|
||||
@@ -29,10 +31,11 @@ COPY cue.mod cue.mod
|
||||
COPY packages/grafana-schema packages/grafana-schema
|
||||
COPY public/app/plugins public/app/plugins
|
||||
COPY pkg pkg
|
||||
COPY build.go package.json ./
|
||||
COPY .bingo .bingo
|
||||
COPY Makefile build.go package.json ./
|
||||
|
||||
RUN go mod verify
|
||||
RUN go run build.go build
|
||||
RUN make build-go
|
||||
|
||||
# Final stage
|
||||
FROM alpine:3.14.2
|
||||
|
||||
+8
-5
@@ -1,12 +1,14 @@
|
||||
FROM node:14.15.1-slim AS js-builder
|
||||
FROM node:16-alpine3.14 as js-builder
|
||||
|
||||
WORKDIR /usr/src/app/
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
COPY packages packages
|
||||
COPY .yarnrc.yml ./
|
||||
COPY .yarn .yarn
|
||||
COPY plugins-bundled plugins-bundled
|
||||
|
||||
RUN apt-get update && apt-get install -yq git
|
||||
RUN yarn install --pure-lockfile
|
||||
RUN yarn install
|
||||
|
||||
COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./
|
||||
COPY public public
|
||||
@@ -22,7 +24,8 @@ FROM golang:1.17.0 AS go-builder
|
||||
WORKDIR /src/grafana
|
||||
|
||||
COPY go.mod go.sum embed.go ./
|
||||
COPY build.go package.json ./
|
||||
COPY Makefile build.go package.json ./
|
||||
COPY .bingo .bingo
|
||||
COPY pkg pkg/
|
||||
COPY cue cue/
|
||||
COPY cue.mod cue.mod/
|
||||
@@ -30,7 +33,7 @@ COPY packages/grafana-schema packages/grafana-schema/
|
||||
COPY public/app/plugins public/app/plugins/
|
||||
|
||||
RUN go mod verify
|
||||
RUN go run build.go build
|
||||
RUN make build-go
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ upgrading Grafana please check here before creating an issue.
|
||||
|
||||
## Plugin development resources
|
||||
|
||||
- [Grafana plugin developer guide](http://docs.grafana.org/plugins/developing/development/)
|
||||
- [Grafana plugin developer guide](https://grafana.com/docs/grafana/latest/developers/plugins/)
|
||||
- [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack)
|
||||
- [Simple JSON datasource plugin](https://github.com/grafana/simple-json-datasource)
|
||||
|
||||
|
||||
@@ -1582,7 +1582,7 @@ We do _not_ recommend using this option. For more information, refer to [Plugin
|
||||
|
||||
### plugin_admin_enabled
|
||||
|
||||
Available to Grafana administrators only, the plugin admin app is set to `false` by default. Set it to `true` to enable the app.
|
||||
Available to Grafana administrators only, the plugin admin app is set to `true` by default. Set it to `false` to disable the app.
|
||||
|
||||
For more information, refer to [Plugin catalog]({{< relref "../plugins/catalog.md" >}}).
|
||||
|
||||
|
||||
@@ -446,11 +446,12 @@ The following sections detail the supported settings and secure settings for eac
|
||||
|
||||
#### Alert notification `discord`
|
||||
|
||||
| Name | Secure setting |
|
||||
| ---------- | -------------- |
|
||||
| url | yes |
|
||||
| avatar_url | |
|
||||
| content | |
|
||||
| Name | Secure setting |
|
||||
| -------------------- | -------------- |
|
||||
| url | yes |
|
||||
| avatar_url | |
|
||||
| content | |
|
||||
| use_discord_username | |
|
||||
|
||||
#### Alert notification `slack`
|
||||
|
||||
|
||||
@@ -227,11 +227,12 @@ In DingTalk PC Client:
|
||||
To set up Discord, you must create a Discord channel webhook. For instructions on how to create the channel, refer to
|
||||
[Intro to Webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks).
|
||||
|
||||
| Setting | Description |
|
||||
| --------------- | --------------------------------------------------------------------------------- |
|
||||
| Webhook URL | Discord webhook URL. |
|
||||
| Message Content | Mention a group using @ or a user using <@ID> when notifying in a channel. |
|
||||
| Avatar URL | Optionally, provide a URL to an image to use as the avatar for the bot's message. |
|
||||
| Setting | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| Webhook URL | Discord webhook URL. |
|
||||
| Message Content | Mention a group using @ or a user using <@ID> when notifying in a channel. |
|
||||
| Avatar URL | Optionally, provide a URL to an image to use as the avatar for the bot's message. |
|
||||
| Use Discord's Webhook Username | Use the username configured in Discord's webhook settings. Otherwise, the username will be 'Grafana.' |
|
||||
|
||||
Alternately, use the [Slack](#slack) notifier by appending `/slack` to a Discord webhook URL.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ weight = 128
|
||||
|
||||
This topic describes how to enable Grafana 8 alerts as well as the rules and restrictions that govern the migration of existing dashboard alerts to this new alerting system. You can also [disable Grafana 8 alerts]({{< relref "./opt-in.md#disable-grafana-8-alerts" >}}) if needed.
|
||||
|
||||
Before you begin, we recommend that you backup Grafana's database. If you are using PostgreSQL as the backend data source, then the minimum required version is 9.5.
|
||||
Before you begin, we recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5.
|
||||
|
||||
## Enable Grafana 8 alerts
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ docker run -d -p 3000:3000 grafana/grafana-enterprise
|
||||
|
||||
### Run a specific version of Grafana
|
||||
|
||||
> **Note:** If you are on a Linux system, you might need to add `sudo` before the command add your user to the `docker` group.
|
||||
> **Note:** If you are on a Linux system, you might need to add `sudo` before the command or add your user to the `docker` group.
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:3000 --name grafana grafana/grafana-enterprise:<version number>
|
||||
|
||||
@@ -10,8 +10,13 @@ Use the legend to adjust how a visualization displays series. This legend functi
|
||||
|
||||
This topic currently applies to the following visualizations:
|
||||
|
||||
- [Bar chart panel]({{< relref "../visualizations/bar-chart.md">}})
|
||||
- [Histogram panel]({{< relref "../visualizations/histogram.md">}})
|
||||
- [Pie chart panel]({{< relref "../visualizations/pie-chart-panel.md">}})
|
||||
- [State timeline panel]({{< relref "../visualizations/state-timeline.md">}})
|
||||
- [Status history panel]({{< relref "../visualizations/status-history.md">}})
|
||||
- [Time series panel]({{< relref "../visualizations/time-series/_index.md" >}})
|
||||
- XY chart panel
|
||||
|
||||
## Toggle series
|
||||
|
||||
@@ -34,3 +39,12 @@ This creates a system override that hides the other series. You can view this ov
|
||||
Click on the series icon (colored line beside the series label) in the legend to change selected series color.
|
||||
|
||||

|
||||
|
||||
## Sort series
|
||||
|
||||
Change legend mode to **Table** and choose [calculations]({{< relref "./calculations-list.md" >}}) to be displayed in the legend. Click the calculation name header in the legend table to sort the values in the table in ascending or descending order.
|
||||
The sort order affects the positions of the bars in the Bar chart panel as well as the order of stacked series in the Time series and Bar chart panels.
|
||||
|
||||
> **Note:** This feature is only supported in these panels: Bar chart, Histogram, Time series, XY Chart.
|
||||
|
||||

|
||||
|
||||
@@ -46,7 +46,7 @@ We’ve continued to bolster the new, unified alerting system launched in Grafan
|
||||
|
||||
## Image Renderer performance improvements and measurement
|
||||
|
||||
You can use Grafana’s image renderer to generate JPEG and PDF images of panels and dashboards. Use these images for alert notifications, PDF exports, and reports sent by Grafana. We’ve added additional metrics to the image renderer to help you diagnose its performance, and [included guidance in our documentation](https://grafana.com/docs/grafana/next/image-rendering/#rendering-mode) to help you configure it for the best mix of performance and resource usage. Tests show that we have reduced image load time from the 95th percentile of 10 seconds to less than 3 seconds under normal load.
|
||||
You can use Grafana’s image renderer to generate images of panels and dashboards. Grafana uses these images for alert notifications, PDF exports (Grafana Enterprise), and reports sent by Grafana (Grafana Enterprise). We’ve added additional metrics to the image renderer to help you diagnose its performance, and [included guidance in our documentation](https://grafana.com/docs/grafana/next/image-rendering/#rendering-mode) to help you configure it for the best mix of performance and resource usage. Tests show that we have reduced image load time from the 95th percentile of 10 seconds to less than 3 seconds under normal load.
|
||||
|
||||
# Grafana Enterprise
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
const PANEL_UNDER_TEST = 'Interpolation: linear';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Visualization suggestions',
|
||||
itName: 'Should be shown and clickable',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: true,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: 'TkZXxlNG3' });
|
||||
e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST);
|
||||
|
||||
// Try visualization suggestions
|
||||
e2e.components.PanelEditor.toggleVizPicker().click();
|
||||
e2e().contains('Suggestions').click();
|
||||
cy.wait(1000);
|
||||
|
||||
// Verify we see suggestions
|
||||
e2e.components.VisualizationPreview.card('Line chart').should('be.visible');
|
||||
|
||||
// Verify search works
|
||||
e2e().get('[placeholder="Search for..."]').type('Table');
|
||||
// Should no longer see line chart
|
||||
e2e.components.VisualizationPreview.card('Line chart').should('not.exist');
|
||||
|
||||
// Select a visualisation
|
||||
e2e.components.VisualizationPreview.card('Table').click();
|
||||
e2e.components.Panels.Visualization.Table.header().should('be.visible');
|
||||
},
|
||||
});
|
||||
@@ -265,4 +265,4 @@ replace gopkg.in/macaron.v1 => ./pkg/macaron
|
||||
|
||||
replace github.com/go-macaron/binding => ./pkg/macaron/binding
|
||||
|
||||
replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.9.8
|
||||
replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.10.2
|
||||
|
||||
+2
-2
@@ -160,8 +160,8 @@
|
||||
"es6-promise": "4.2.8",
|
||||
"es6-shim": "0.35.5",
|
||||
"eslint": "7.21.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-jsdoc": "36.1.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-jsdoc": "37.0.0",
|
||||
"eslint-plugin-lodash": "^7.2.0",
|
||||
"eslint-plugin-no-only-tests": "2.4.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "5.0.2",
|
||||
"@grafana/schema": "8.3.0-pre",
|
||||
"@types/d3-interpolate": "^1.3.1",
|
||||
"@types/d3-interpolate": "^3.0.0",
|
||||
"date-fns": "2.21.3",
|
||||
"eventemitter3": "4.0.7",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PanelTypeChangedHandler,
|
||||
FieldConfigProperty,
|
||||
PanelPluginDataSupport,
|
||||
VisualizationSuggestionsSupplier,
|
||||
} from '../types';
|
||||
import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
|
||||
import { ComponentClass, ComponentType } from 'react';
|
||||
@@ -104,6 +105,7 @@ export class PanelPlugin<
|
||||
};
|
||||
|
||||
private optionsSupplier?: PanelOptionsSupplier<TOptions>;
|
||||
private suggestionsSupplier?: VisualizationSuggestionsSupplier;
|
||||
|
||||
panel: ComponentType<PanelProps<TOptions>> | null;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
@@ -354,4 +356,21 @@ export class PanelPlugin<
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets function that can return visualization examples and suggestions.
|
||||
* @alpha
|
||||
*/
|
||||
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplier) {
|
||||
this.suggestionsSupplier = supplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suggestions supplier
|
||||
* @alpha
|
||||
*/
|
||||
getSuggestionsSupplier(): VisualizationSuggestionsSupplier | undefined {
|
||||
return this.suggestionsSupplier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export enum DashboardCursorSync {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PanelModel<TOptions = any, TCustomFieldConfig extends object = any> {
|
||||
export interface PanelModel<TOptions = any, TCustomFieldConfig = any> {
|
||||
/** ID of the panel within the current dashboard */
|
||||
id: number;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export enum FieldType {
|
||||
*
|
||||
* Plugins may extend this with additional properties. Something like series overrides
|
||||
*/
|
||||
export interface FieldConfig<TOptions extends object = any> {
|
||||
export interface FieldConfig<TOptions = any> {
|
||||
/**
|
||||
* The display value for this field. This supports template variables blank is auto
|
||||
*/
|
||||
|
||||
@@ -49,7 +49,7 @@ export const isSystemOverride = (override: ConfigOverrideRule): override is Syst
|
||||
return typeof (override as SystemConfigOverrideRule)?.__systemRef === 'string';
|
||||
};
|
||||
|
||||
export interface FieldConfigSource<TOptions extends object = any> {
|
||||
export interface FieldConfigSource<TOptions = any> {
|
||||
// Defaults applied to all numeric fields
|
||||
defaults: FieldConfig<TOptions>;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource
|
||||
import { PluginMeta } from './plugin';
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { LoadingState } from './data';
|
||||
import { DataFrame } from './dataFrame';
|
||||
import { DataFrame, FieldType } from './dataFrame';
|
||||
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
||||
import { EventBus } from '../events';
|
||||
import { FieldConfigSource } from './fieldOverrides';
|
||||
@@ -12,6 +12,8 @@ import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
|
||||
import { OptionEditorConfig } from './options';
|
||||
import { AlertStateInfo } from './alerts';
|
||||
import { PanelModel } from './dashboard';
|
||||
import { DataTransformerConfig } from './transformations';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
|
||||
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
|
||||
|
||||
@@ -58,7 +60,7 @@ export interface PanelData {
|
||||
timeRange: TimeRange;
|
||||
}
|
||||
|
||||
export interface PanelProps<T = any, S = any> {
|
||||
export interface PanelProps<T = any> {
|
||||
/** ID of the panel within the current dashboard */
|
||||
id: number;
|
||||
|
||||
@@ -182,3 +184,137 @@ export interface PanelPluginDataSupport {
|
||||
annotations: boolean;
|
||||
alertStates: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface VisualizationSuggestion<TOptions = any, TFieldConfig = any> {
|
||||
/** Name of suggestion */
|
||||
name: string;
|
||||
/** Description */
|
||||
description?: string;
|
||||
/** Panel plugin id */
|
||||
pluginId: string;
|
||||
/** Panel plugin options */
|
||||
options?: Partial<TOptions>;
|
||||
/** Panel plugin field options */
|
||||
fieldConfig?: FieldConfigSource<Partial<TFieldConfig>>;
|
||||
/** Data transformations */
|
||||
transformations?: DataTransformerConfig[];
|
||||
/** Tweak for small preview */
|
||||
previewModifier?: (suggestion: VisualizationSuggestion) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface PanelDataSummary {
|
||||
hasData?: boolean;
|
||||
rowCountTotal: number;
|
||||
rowCountMax: number;
|
||||
frameCount: number;
|
||||
numberFieldCount: number;
|
||||
timeFieldCount: number;
|
||||
stringFieldCount: number;
|
||||
hasNumberField?: boolean;
|
||||
hasTimeField?: boolean;
|
||||
hasStringField?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export class VisualizationSuggestionsBuilder {
|
||||
/** Current data */
|
||||
data?: PanelData;
|
||||
/** Current panel & options */
|
||||
panel?: PanelModel;
|
||||
/** Summary stats for current data */
|
||||
dataSummary: PanelDataSummary;
|
||||
|
||||
private list: VisualizationSuggestion[] = [];
|
||||
|
||||
constructor(data?: PanelData, panel?: PanelModel) {
|
||||
this.data = data;
|
||||
this.panel = panel;
|
||||
this.dataSummary = this.computeDataSummary();
|
||||
}
|
||||
|
||||
getListAppender<TOptions, TFieldConfig>(defaults: VisualizationSuggestion<TOptions, TFieldConfig>) {
|
||||
return new VisualizationSuggestionsListAppender<TOptions, TFieldConfig>(this.list, defaults);
|
||||
}
|
||||
|
||||
private computeDataSummary() {
|
||||
const frames = this.data?.series || [];
|
||||
|
||||
let numberFieldCount = 0;
|
||||
let timeFieldCount = 0;
|
||||
let stringFieldCount = 0;
|
||||
let rowCountTotal = 0;
|
||||
let rowCountMax = 0;
|
||||
|
||||
for (const frame of frames) {
|
||||
rowCountTotal += frame.length;
|
||||
|
||||
for (const field of frame.fields) {
|
||||
switch (field.type) {
|
||||
case FieldType.number:
|
||||
numberFieldCount += 1;
|
||||
break;
|
||||
case FieldType.time:
|
||||
timeFieldCount += 1;
|
||||
break;
|
||||
case FieldType.string:
|
||||
stringFieldCount += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (frame.length > rowCountMax) {
|
||||
rowCountMax = frame.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
numberFieldCount,
|
||||
timeFieldCount,
|
||||
stringFieldCount,
|
||||
rowCountTotal,
|
||||
rowCountMax,
|
||||
frameCount: frames.length,
|
||||
hasData: rowCountTotal > 0,
|
||||
hasTimeField: timeFieldCount > 0,
|
||||
hasNumberField: numberFieldCount > 0,
|
||||
hasStringField: stringFieldCount > 0,
|
||||
};
|
||||
}
|
||||
|
||||
getList() {
|
||||
return this.list;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type VisualizationSuggestionsSupplier = {
|
||||
/**
|
||||
* Adds good suitable suggestions for the current data
|
||||
*/
|
||||
getSuggestionsForData: (builder: VisualizationSuggestionsBuilder) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helps with typings and defaults
|
||||
* @alpha
|
||||
*/
|
||||
export class VisualizationSuggestionsListAppender<TOptions, TFieldConfig> {
|
||||
constructor(
|
||||
private list: VisualizationSuggestion[],
|
||||
private defaults: VisualizationSuggestion<TOptions, TFieldConfig>
|
||||
) {}
|
||||
|
||||
append(overrides: Partial<VisualizationSuggestion<TOptions, TFieldConfig>>) {
|
||||
this.list.push(defaultsDeep(overrides, this.defaults));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,4 +262,7 @@ export const Components = {
|
||||
PanelAlertTabContent: {
|
||||
content: 'Unified alert editor tab content',
|
||||
},
|
||||
VisualizationPreview: {
|
||||
card: (name: string) => `data-testid suggestion-${name}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,10 +13,10 @@ export interface PanelRendererProps<P extends object = any, F extends object = a
|
||||
data: PanelData;
|
||||
pluginId: string;
|
||||
title: string;
|
||||
options?: P;
|
||||
options?: Partial<P>;
|
||||
onOptionsChange?: (options: P) => void;
|
||||
onChangeTimeRange?: (timeRange: AbsoluteTimeRange) => void;
|
||||
fieldConfig?: FieldConfigSource<F>;
|
||||
fieldConfig?: FieldConfigSource<Partial<F>>;
|
||||
timeZone?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
@@ -258,6 +258,8 @@ export interface VizLegendOptions {
|
||||
displayMode: LegendDisplayMode;
|
||||
isVisible?: boolean;
|
||||
placement: LegendPlacement;
|
||||
sortBy?: string;
|
||||
sortDesc?: boolean;
|
||||
}
|
||||
|
||||
export enum BarGaugeDisplayMode {
|
||||
|
||||
@@ -5,9 +5,11 @@ LegendPlacement: "bottom" | "right" @cuetsy(kind="type")
|
||||
LegendDisplayMode: "list" | "table" | "hidden" @cuetsy(kind="enum")
|
||||
|
||||
VizLegendOptions: {
|
||||
displayMode: LegendDisplayMode
|
||||
placement: LegendPlacement
|
||||
displayMode: LegendDisplayMode
|
||||
placement: LegendPlacement
|
||||
asTable?: bool
|
||||
isVisible?: bool
|
||||
calcs: [...string]
|
||||
sortBy?: string
|
||||
sortDesc?: bool
|
||||
calcs: [...string]
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
@@ -51,12 +51,12 @@
|
||||
"chalk": "^2.4.2",
|
||||
"command-exists": "^1.2.8",
|
||||
"commander": "^5.0.0",
|
||||
"concurrently": "4.1.0",
|
||||
"concurrently": "6.3.0",
|
||||
"copy-webpack-plugin": "5.1.2",
|
||||
"css-loader": "3.4.2",
|
||||
"eslint": "7.21.0",
|
||||
"execa": "^5.1.1",
|
||||
"file-loader": "5.0.2",
|
||||
"file-loader": "6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "1.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"globby": "^10.0.1",
|
||||
|
||||
@@ -1,47 +1,48 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { HTMLProps } from 'react';
|
||||
import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data';
|
||||
import { Button, Icon, Input } from '..';
|
||||
import { useFocus } from '../Input/utils';
|
||||
import { useCombinedRefs } from '../../utils/useCombinedRefs';
|
||||
|
||||
export interface Props {
|
||||
export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'onChange'> {
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
width?: number;
|
||||
onChange: (value: string) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => {
|
||||
const [inputRef, setInputFocus] = useFocus();
|
||||
const suffix =
|
||||
value !== '' ? (
|
||||
<Button
|
||||
icon="times"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
setInputFocus();
|
||||
onChange('');
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
) : null;
|
||||
export const FilterInput = React.forwardRef<HTMLInputElement, Props>(
|
||||
({ value, width, onChange, ...restProps }, ref) => {
|
||||
const innerRef = React.useRef<HTMLInputElement>(null);
|
||||
const combinedRef = useCombinedRefs(ref, innerRef) as React.Ref<HTMLInputElement>;
|
||||
|
||||
return (
|
||||
<Input
|
||||
autoFocus={autoFocus ?? false}
|
||||
prefix={<Icon name="search" />}
|
||||
ref={inputRef}
|
||||
suffix={suffix}
|
||||
width={width}
|
||||
type="text"
|
||||
value={value ? unEscapeStringFromRegex(value) : ''}
|
||||
onChange={(event) => onChange(escapeStringForRegex(event.currentTarget.value))}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const suffix =
|
||||
value !== '' ? (
|
||||
<Button
|
||||
icon="times"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
innerRef.current?.focus();
|
||||
onChange('');
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Input
|
||||
prefix={<Icon name="search" />}
|
||||
suffix={suffix}
|
||||
width={width}
|
||||
type="text"
|
||||
value={value ? unEscapeStringFromRegex(value) : ''}
|
||||
onChange={(event) => onChange(escapeStringForRegex(event.currentTarget.value))}
|
||||
{...restProps}
|
||||
ref={combinedRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FilterInput.displayName = 'FilterInput';
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Themeable2 } from '../../types';
|
||||
|
||||
import { CodeEditorProps, Monaco, MonacoEditor as MonacoEditorType, MonacoOptions } from './types';
|
||||
import { registerSuggestions } from './suggestions';
|
||||
import defineThemes from './theme';
|
||||
|
||||
type Props = CodeEditorProps & Themeable2;
|
||||
|
||||
@@ -85,8 +84,7 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
|
||||
handleBeforeMount = (monaco: Monaco) => {
|
||||
this.monaco = monaco;
|
||||
const { language, theme, getSuggestions, onBeforeEditorMount } = this.props;
|
||||
defineThemes(monaco, theme);
|
||||
const { language, getSuggestions, onBeforeEditorMount } = this.props;
|
||||
|
||||
if (getSuggestions) {
|
||||
this.completionCancel = registerSuggestions(monaco, language, getSuggestions);
|
||||
@@ -148,7 +146,6 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
width={width}
|
||||
height={height}
|
||||
language={language}
|
||||
theme={theme.isDark ? 'grafana-dark' : 'grafana-light'}
|
||||
value={value}
|
||||
options={{
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
import MonacoEditor, { loader as monacoEditorLoader, EditorProps as MonacoEditorProps } from '@monaco-editor/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import MonacoEditor, { loader as monacoEditorLoader, useMonaco } from '@monaco-editor/react';
|
||||
import defineThemes from './theme';
|
||||
import { useTheme2 } from '../../themes';
|
||||
import type { ReactMonacoEditorProps } from './types';
|
||||
|
||||
let initalized = false;
|
||||
function initMonaco() {
|
||||
@@ -13,9 +16,30 @@ function initMonaco() {
|
||||
},
|
||||
});
|
||||
initalized = true;
|
||||
monacoEditorLoader.init().then((monaco) => {
|
||||
// this call makes sure the themes exist.
|
||||
// they will not have the correct colors,
|
||||
// but we need them to exist since the beginning,
|
||||
// because if we start a monaco instance with
|
||||
// a theme that does not exist, it will not work well.
|
||||
defineThemes(monaco);
|
||||
});
|
||||
}
|
||||
|
||||
export const ReactMonacoEditor = (props: MonacoEditorProps) => {
|
||||
export const ReactMonacoEditor = (props: ReactMonacoEditorProps) => {
|
||||
const theme = useTheme2();
|
||||
const monaco = useMonaco();
|
||||
|
||||
useEffect(() => {
|
||||
// monaco can be null at the beginning, because it is loaded in asynchronously
|
||||
if (monaco !== null) {
|
||||
defineThemes(monaco, theme);
|
||||
}
|
||||
}, [monaco, theme]);
|
||||
|
||||
initMonaco();
|
||||
return <MonacoEditor {...props} />;
|
||||
|
||||
const monacoTheme = theme.isDark ? 'grafana-dark' : 'grafana-light';
|
||||
|
||||
return <MonacoEditor theme={monacoTheme} {...props} />;
|
||||
};
|
||||
|
||||
@@ -2,13 +2,13 @@ import React from 'react';
|
||||
import { useAsyncDependency } from '../../utils/useAsyncDependency';
|
||||
import { ErrorWithStack, LoadingPlaceholder } from '..';
|
||||
// we only use import type so it will not be included in the bundle
|
||||
import type { EditorProps } from '@monaco-editor/react';
|
||||
import type { ReactMonacoEditorProps } from './types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Experimental export
|
||||
**/
|
||||
export const ReactMonacoEditorLazy = (props: EditorProps) => {
|
||||
export const ReactMonacoEditorLazy = (props: ReactMonacoEditorProps) => {
|
||||
const { loading, error, dependency } = useAsyncDependency(
|
||||
import(/* webpackChunkName: "react-monaco-editor" */ './ReactMonacoEditor')
|
||||
);
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Monaco } from './types';
|
||||
import { Monaco, monacoTypes } from './types';
|
||||
|
||||
export default function defineThemes(monaco: Monaco, theme: GrafanaTheme2) {
|
||||
function getColors(theme?: GrafanaTheme2): monacoTypes.editor.IColors {
|
||||
if (theme === undefined) {
|
||||
return {};
|
||||
} else {
|
||||
return {
|
||||
'editor.background': theme.components.input.background,
|
||||
'minimap.background': theme.colors.background.secondary,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// we support calling this without a theme, it will make sure the themes
|
||||
// are registered in monaco, even if the colors are not perfect.
|
||||
export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) {
|
||||
// color tokens are defined here https://github.com/microsoft/vscode/blob/main/src/vs/platform/theme/common/colorRegistry.ts#L174
|
||||
const colors = {
|
||||
'editor.background': theme.components.input.background,
|
||||
'minimap.background': theme.colors.background.secondary,
|
||||
};
|
||||
const colors = getColors(theme);
|
||||
|
||||
monaco.editor.defineTheme('grafana-dark', {
|
||||
base: 'vs-dark',
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
// We use `import type` to guarantee it'll be erased from the JS and it doesnt accidently bundle monaco
|
||||
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import type { EditorProps } from '@monaco-editor/react';
|
||||
|
||||
// we do not allow customizing the theme.
|
||||
// (theme is complicated in Monaco, right now there is
|
||||
// a limitation where all monaco editors must have
|
||||
// the same theme, see
|
||||
// https://github.com/microsoft/monaco-editor/issues/338#issuecomment-274837186
|
||||
// )
|
||||
export type ReactMonacoEditorProps = Omit<EditorProps, 'theme'>;
|
||||
|
||||
export type CodeEditorChangeHandler = (value: string) => void;
|
||||
export type CodeEditorSuggestionProvider = () => CodeEditorSuggestionItem[];
|
||||
|
||||
@@ -59,6 +59,11 @@ export interface PanelContext {
|
||||
|
||||
/** Update instance state, this is only supported in dashboard panel context currently */
|
||||
onInstanceStateChange?: (state: any) => void;
|
||||
|
||||
/**
|
||||
* Called when a panel is changing the sort order of the legends.
|
||||
*/
|
||||
onToggleLegendSort?: (sortBy: string) => void;
|
||||
}
|
||||
|
||||
export const PanelContextRoot = React.createContext<PanelContext>({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { preparePlotConfigBuilder } from './utils';
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
|
||||
|
||||
const propsToDiff: string[] = [];
|
||||
const propsToDiff: string[] = ['legend'];
|
||||
|
||||
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
|
||||
|
||||
@@ -18,7 +18,7 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
|
||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const { eventBus, sync } = this.context;
|
||||
const { theme, timeZone } = this.props;
|
||||
const { theme, timeZone, legend } = this.props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
@@ -28,6 +28,7 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
eventBus,
|
||||
sync,
|
||||
allFrames,
|
||||
legend,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ import {
|
||||
VisibilityMode,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
VizLegendOptions,
|
||||
} from '@grafana/schema';
|
||||
import { collectStackingGroups, preparePlotData } from '../uPlot/utils';
|
||||
import { collectStackingGroups, orderIdsByCalcs, preparePlotData } from '../uPlot/utils';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||
@@ -35,7 +36,7 @@ const defaultConfig: GraphFieldConfig = {
|
||||
axisPlacement: AxisPlacement.Auto,
|
||||
};
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursorSync }> = ({
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursorSync; legend?: VizLegendOptions }> = ({
|
||||
frame,
|
||||
theme,
|
||||
timeZone,
|
||||
@@ -43,10 +44,11 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
eventBus,
|
||||
sync,
|
||||
allFrames,
|
||||
legend,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
builder.setPrepData(preparePlotData);
|
||||
builder.setPrepData((prepData) => preparePlotData(prepData, undefined, legend));
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0];
|
||||
@@ -265,7 +267,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
builder.setStacking(true);
|
||||
for (const [_, seriesIdxs] of stackingGroups.entries()) {
|
||||
for (const [_, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
|
||||
@@ -23,7 +23,7 @@ export function VizLegend<T>({
|
||||
itemRenderer,
|
||||
readonly,
|
||||
}: LegendProps<T>) {
|
||||
const { eventBus, onToggleSeriesVisibility } = usePanelContext();
|
||||
const { eventBus, onToggleSeriesVisibility, onToggleLegendSort } = usePanelContext();
|
||||
|
||||
const onMouseEnter = useCallback(
|
||||
(item: VizLegendItem, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
@@ -82,7 +82,7 @@ export function VizLegend<T>({
|
||||
sortBy={sortKey}
|
||||
sortDesc={sortDesc}
|
||||
onLabelClick={onLegendLabelClick}
|
||||
onToggleSort={onToggleSort}
|
||||
onToggleSort={onToggleSort || onToggleLegendSort}
|
||||
onLabelMouseEnter={onMouseEnter}
|
||||
onLabelMouseOut={onMouseOut}
|
||||
itemRenderer={itemRenderer}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { VizLegendTableProps } from './types';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { sortBy } from 'lodash';
|
||||
import { orderBy } from 'lodash';
|
||||
import { LegendTableItem } from './VizLegendTableItem';
|
||||
import { DisplayValue, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
@@ -34,13 +34,17 @@ export const VizLegendTable = <T extends unknown>({
|
||||
}
|
||||
|
||||
const sortedItems = sortKey
|
||||
? sortBy(items, (item) => {
|
||||
if (item.getDisplayValues) {
|
||||
const stat = item.getDisplayValues().filter((stat) => stat.title === sortKey)[0];
|
||||
return stat && stat.numeric;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
? orderBy(
|
||||
items,
|
||||
(item) => {
|
||||
if (item.getDisplayValues) {
|
||||
const stat = item.getDisplayValues().filter((stat) => stat.title === sortKey)[0];
|
||||
return stat && stat.numeric;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
sortDesc ? 'desc' : 'asc'
|
||||
)
|
||||
: items;
|
||||
|
||||
if (!itemRenderer) {
|
||||
@@ -68,7 +72,9 @@ export const VizLegendTable = <T extends unknown>({
|
||||
<th
|
||||
title={displayValue.description}
|
||||
key={columnTitle}
|
||||
className={cx(styles.header, onToggleSort && styles.headerSortable)}
|
||||
className={cx(styles.header, onToggleSort && styles.headerSortable, {
|
||||
[styles.withIcon]: sortKey === columnTitle,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (onToggleSort) {
|
||||
onToggleSort(columnTitle);
|
||||
@@ -76,9 +82,7 @@ export const VizLegendTable = <T extends unknown>({
|
||||
}}
|
||||
>
|
||||
{columnTitle}
|
||||
{sortKey === columnTitle && (
|
||||
<Icon className={styles.sortIcon} name={sortDesc ? 'angle-down' : 'angle-up'} />
|
||||
)}
|
||||
{sortKey === columnTitle && <Icon size="xs" name={sortDesc ? 'angle-down' : 'angle-up'} />}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
@@ -94,21 +98,23 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
width: 100%;
|
||||
th:first-child {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid ${theme.colors.border.weak};
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
color: ${theme.colors.primary.text};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
border-bottom: 1px solid ${theme.colors.border.weak};
|
||||
padding: ${theme.spacing(0.25, 1)};
|
||||
padding: ${theme.spacing(0.25, 2, 0.25, 1)};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
// This needs to be padding-right - icon size(xs==12) to avoid jumping
|
||||
withIcon: css`
|
||||
padding-right: 4px;
|
||||
`,
|
||||
headerSortable: css`
|
||||
cursor: pointer;
|
||||
`,
|
||||
sortIcon: css`
|
||||
margin-left: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
align-items: center;
|
||||
`,
|
||||
value: css`
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
`,
|
||||
yAxisLabel: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
|
||||
@@ -85,7 +85,13 @@ export const PlotLegend: React.FC<PlotLegendProps> = ({
|
||||
|
||||
return (
|
||||
<VizLayout.Legend placement={placement} {...vizLayoutLegendProps}>
|
||||
<VizLegend placement={placement} items={legendItems} displayMode={displayMode} />
|
||||
<VizLegend
|
||||
placement={placement}
|
||||
items={legendItems}
|
||||
displayMode={displayMode}
|
||||
sortBy={vizLayoutLegendProps.sortBy}
|
||||
sortDesc={vizLayoutLegendProps.sortDesc}
|
||||
/>
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { preparePlotData, timeFormatToTemplate } from './utils';
|
||||
import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils';
|
||||
import { FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { StackingMode } from '@grafana/schema';
|
||||
|
||||
@@ -295,5 +295,113 @@ describe('preparePlotData', () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('with legend sorted', () => {
|
||||
it('should affect when single group', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, 20, 10],
|
||||
state: { calcs: { max: 20 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, 10, 10],
|
||||
state: { calcs: { max: 10 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, 20, 20],
|
||||
state: { calcs: { max: 20 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: false } as any)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
30,
|
||||
20,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
50,
|
||||
40,
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: true } as any)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
20,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
50,
|
||||
40,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
40,
|
||||
30,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('orderIdsByCalcs', () => {
|
||||
const ids = [1, 2, 3, 4];
|
||||
const frame = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{ name: 'a', values: [-10, 20, 10], state: { calcs: { min: -10 } } },
|
||||
{ name: 'b', values: [20, 20, 20], state: { calcs: { min: 20 } } },
|
||||
{ name: 'c', values: [10, 10, 10], state: { calcs: { min: 10 } } },
|
||||
{ name: 'd', values: [30, 30, 30] },
|
||||
],
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ legend: undefined },
|
||||
{ legend: { sortBy: 'Min' } },
|
||||
{ legend: { sortDesc: false } },
|
||||
{ legend: {} },
|
||||
{ sortBy: 'Mik', sortDesc: true },
|
||||
])('should return without ordering if legend option is %o', (legend: any) => {
|
||||
const result = orderIdsByCalcs({ ids, frame, legend });
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should order the ids based on the frame stat', () => {
|
||||
const resultDesc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: true } as any });
|
||||
expect(resultDesc).toEqual([4, 2, 3, 1]);
|
||||
const resultAsc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: false } as any });
|
||||
expect(resultAsc).toEqual([1, 3, 2, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DataFrame, ensureTimeField, Field, FieldType } from '@grafana/data';
|
||||
import { StackingMode } from '@grafana/schema';
|
||||
import { createLogger } from '../../utils/logger';
|
||||
import { attachDebugger } from '../../utils';
|
||||
import { StackingMode, VizLegendOptions } from '@grafana/schema';
|
||||
import { orderBy } from 'lodash';
|
||||
import { AlignedData, Options, PaddingSide } from 'uplot';
|
||||
import { attachDebugger } from '../../utils';
|
||||
import { createLogger } from '../../utils/logger';
|
||||
|
||||
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
|
||||
|
||||
@@ -39,7 +40,11 @@ interface StackMeta {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData(frames: DataFrame[], onStackMeta?: (meta: StackMeta) => void): AlignedData {
|
||||
export function preparePlotData(
|
||||
frames: DataFrame[],
|
||||
onStackMeta?: (meta: StackMeta) => void,
|
||||
legend?: VizLegendOptions
|
||||
): AlignedData {
|
||||
const frame = frames[0];
|
||||
const result: any[] = [];
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
@@ -67,7 +72,9 @@ export function preparePlotData(frames: DataFrame[], onStackMeta?: (meta: StackM
|
||||
alignedTotals[0] = null;
|
||||
|
||||
// array or stacking groups
|
||||
for (const [_, seriesIdxs] of stackingGroups.entries()) {
|
||||
for (const [_, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
|
||||
const groupTotals = byPct ? Array(dataLength).fill(0) : null;
|
||||
|
||||
if (byPct) {
|
||||
@@ -184,3 +191,23 @@ export const pluginLogger = createLogger('uPlot');
|
||||
export const pluginLog = pluginLogger.logger;
|
||||
// pluginLogger.enable();
|
||||
attachDebugger('graphng', undefined, pluginLogger);
|
||||
|
||||
type OrderIdsByCalcsOptions = {
|
||||
legend?: VizLegendOptions;
|
||||
ids: number[];
|
||||
frame: DataFrame;
|
||||
};
|
||||
export function orderIdsByCalcs({ legend, ids, frame }: OrderIdsByCalcsOptions) {
|
||||
if (!legend?.sortBy || legend.sortDesc == null) {
|
||||
return ids;
|
||||
}
|
||||
const orderedIds = orderBy<number>(
|
||||
ids,
|
||||
(id) => {
|
||||
return frame.fields[id].state?.calcs?.[legend.sortBy!.toLowerCase()];
|
||||
},
|
||||
legend.sortDesc ? 'desc' : 'asc'
|
||||
);
|
||||
|
||||
return orderedIds;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
export function useCombinedRefs<T>(...refs: any) {
|
||||
const targetRef = React.useRef<T>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
refs.forEach((ref: any) => {
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof ref === 'function') {
|
||||
ref(targetRef.current);
|
||||
} else {
|
||||
ref.current = targetRef.current;
|
||||
}
|
||||
});
|
||||
}, [refs]);
|
||||
|
||||
return targetRef;
|
||||
}
|
||||
@@ -2,6 +2,6 @@ import { useState } from 'react';
|
||||
|
||||
/** @internal */
|
||||
export function useForceUpdate() {
|
||||
const [value, setValue] = useState(0); // integer state
|
||||
return () => setValue(value + 1); // update the state to force render
|
||||
const [_, setValue] = useState(0); // integer state
|
||||
return () => setValue((prevState) => prevState + 1); // update the state to force render
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
for _, method := range strings.Split(route.Method, ",") {
|
||||
r.Handle(strings.TrimSpace(method), url, handlers)
|
||||
}
|
||||
log.Debugf("Plugins: Adding proxy route %s", url)
|
||||
log.Debug("Plugins: Adding proxy route", "url", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) {
|
||||
if avatar.Expired() {
|
||||
// The cache item is either expired or newly created, update it from the server
|
||||
if err := avatar.Update(); err != nil {
|
||||
log.Tracef("avatar update error: %v", err)
|
||||
log.Debug("avatar update", "err", err)
|
||||
avatar = a.notFound
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) {
|
||||
avatar = a.notFound
|
||||
} else if !exists {
|
||||
if err := a.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil {
|
||||
log.Tracef("Error adding avatar to cache: %s", err)
|
||||
log.Debug("add avatar to cache", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) {
|
||||
ctx.Resp.Header().Set("Cache-Control", "private, max-age=3600")
|
||||
|
||||
if err := avatar.Encode(ctx.Resp); err != nil {
|
||||
log.Warnf("avatar encode error: %v", err)
|
||||
log.Warn("avatar encode error:", "err", err)
|
||||
ctx.Resp.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func newNotFound(cfg *setting.Cfg) *Avatar {
|
||||
// variable.
|
||||
// nolint:gosec
|
||||
if data, err := ioutil.ReadFile(path); err != nil {
|
||||
log.Errorf(3, "Failed to read user_profile.png, %v", path)
|
||||
log.Error("Failed to read user_profile.png", "path", path)
|
||||
} else {
|
||||
avatar.data = bytes.NewBuffer(data)
|
||||
}
|
||||
@@ -215,7 +215,7 @@ var client = &http.Client{
|
||||
func (a *thunderTask) fetch() error {
|
||||
a.Avatar.timestamp = time.Now()
|
||||
|
||||
log.Debugf("avatar.fetch(fetch new avatar): %s", a.Url)
|
||||
log.Debug("avatar.fetch(fetch new avatar)", "url", a.Url)
|
||||
req, err := http.NewRequest("GET", a.Url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -65,7 +65,7 @@ func GetGravatarUrl(text string) string {
|
||||
|
||||
hasher := md5.New()
|
||||
if _, err := hasher.Write([]byte(strings.ToLower(text))); err != nil {
|
||||
log.Warnf("Failed to hash text: %s", err)
|
||||
log.Warn("Failed to hash text", "err", err)
|
||||
}
|
||||
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plu
|
||||
|
||||
meta, exists := enabledPlugins.DataSources[ds.Type]
|
||||
if !exists {
|
||||
log.Errorf(3, "Could not find plugin definition for data source: %v", ds.Type)
|
||||
log.Error("Could not find plugin definition for data source", "datasource_type", ds.Type)
|
||||
continue
|
||||
}
|
||||
dsMap["meta"] = meta
|
||||
|
||||
+4
-4
@@ -131,7 +131,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
|
||||
if err := hs.ValidateRedirectTo(redirectTo); err != nil {
|
||||
// the user is already logged so instead of rendering the login page with error
|
||||
// it should be redirected to the home page.
|
||||
log.Debugf("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
||||
log.Debug("Ignored invalid redirect_to cookie value", "redirect_to", redirectTo)
|
||||
redirectTo = hs.Cfg.AppSubURL + "/"
|
||||
}
|
||||
cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
|
||||
@@ -152,12 +152,12 @@ func (hs *HTTPServer) tryOAuthAutoLogin(c *models.ReqContext) bool {
|
||||
}
|
||||
oauthInfos := hs.SocialService.GetOAuthInfoProviders()
|
||||
if len(oauthInfos) != 1 {
|
||||
log.Warnf("Skipping OAuth auto login because multiple OAuth providers are configured")
|
||||
log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured")
|
||||
return false
|
||||
}
|
||||
for key := range oauthInfos {
|
||||
redirectUrl := hs.Cfg.AppSubURL + "/login/" + key
|
||||
log.Infof("OAuth auto login enabled. Redirecting to " + redirectUrl)
|
||||
log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
|
||||
c.Redirect(redirectUrl, 307)
|
||||
return true
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response {
|
||||
if err := hs.ValidateRedirectTo(redirectTo); err == nil {
|
||||
result["redirectUrl"] = redirectTo
|
||||
} else {
|
||||
log.Infof("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
||||
log.Info("Ignored invalid redirect_to cookie value.", "url", redirectTo)
|
||||
}
|
||||
cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
log.Debugf("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
||||
log.Debug("Ignored invalid redirect_to cookie value", "redirect_to", redirectTo)
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubUrl + "/")
|
||||
|
||||
@@ -74,7 +74,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
||||
return "", err
|
||||
}
|
||||
key := u.path + rand + pngExt
|
||||
log.Debugf("Uploading image to s3. bucket = %s, path = %s", u.bucket, key)
|
||||
log.Debug("Uploading image to s3.", "bucket", u.bucket, "path", key)
|
||||
|
||||
// We can ignore the gosec G304 warning on this one because `imageDiskPath` comes
|
||||
// from alert notifiers and is only used to upload images generated by alerting.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type CloseCallbackFunc func(bytesRead int64)
|
||||
|
||||
// CountBytesReader counts the total amount of bytes read from the underlying reader.
|
||||
//
|
||||
// The provided callback func will be called before the underlying reader is closed.
|
||||
func CountBytesReader(reader io.ReadCloser, callback CloseCallbackFunc) io.ReadCloser {
|
||||
if reader == nil {
|
||||
panic("reader cannot be nil")
|
||||
}
|
||||
|
||||
if callback == nil {
|
||||
panic("callback cannot be nil")
|
||||
}
|
||||
|
||||
return &countBytesReader{reader: reader, callback: callback}
|
||||
}
|
||||
|
||||
type countBytesReader struct {
|
||||
reader io.ReadCloser
|
||||
callback CloseCallbackFunc
|
||||
counter int64
|
||||
}
|
||||
|
||||
func (r *countBytesReader) Read(p []byte) (int, error) {
|
||||
n, err := r.reader.Read(p)
|
||||
r.counter += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *countBytesReader) Close() error {
|
||||
r.callback(r.counter)
|
||||
return r.reader.Close()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCountBytesReader(t *testing.T) {
|
||||
tcs := []struct {
|
||||
body string
|
||||
expectedBytesCount int64
|
||||
}{
|
||||
{body: "d", expectedBytesCount: 1},
|
||||
{body: "dummy", expectedBytesCount: 5},
|
||||
}
|
||||
|
||||
for index, tc := range tcs {
|
||||
t.Run(fmt.Sprintf("Test CountBytesReader %d", index), func(t *testing.T) {
|
||||
body := ioutil.NopCloser(strings.NewReader(tc.body))
|
||||
var actualBytesRead int64
|
||||
|
||||
readCloser := CountBytesReader(body, func(bytesRead int64) {
|
||||
actualBytesRead = bytesRead
|
||||
})
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(readCloser)
|
||||
require.NoError(t, err)
|
||||
err = readCloser.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedBytesCount, actualBytesRead)
|
||||
require.Equal(t, string(bodyBytes), tc.body)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ package httpclientprovider
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -56,8 +57,8 @@ const DataSourceMetricsMiddlewareName = "metrics"
|
||||
|
||||
var executeMiddlewareFunc = executeMiddleware
|
||||
|
||||
func DataSourceMetricsMiddleware() httpclient.Middleware {
|
||||
return httpclient.NamedMiddlewareFunc(DataSourceMetricsMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||
func DataSourceMetricsMiddleware() sdkhttpclient.Middleware {
|
||||
return sdkhttpclient.NamedMiddlewareFunc(DataSourceMetricsMiddlewareName, func(opts sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||
if opts.Labels == nil {
|
||||
return next
|
||||
}
|
||||
@@ -81,7 +82,7 @@ func DataSourceMetricsMiddleware() httpclient.Middleware {
|
||||
}
|
||||
|
||||
func executeMiddleware(next http.RoundTripper, datasourceLabel prometheus.Labels) http.RoundTripper {
|
||||
return httpclient.RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return sdkhttpclient.RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
requestCounter := datasourceRequestCounter.MustCurryWith(datasourceLabel)
|
||||
requestSummary := datasourceRequestSummary.MustCurryWith(datasourceLabel)
|
||||
requestInFlight := datasourceRequestsInFlight.With(datasourceLabel)
|
||||
@@ -94,10 +95,11 @@ func executeMiddleware(next http.RoundTripper, datasourceLabel prometheus.Labels
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// we avoid measuring contentlength less than zero because it indicates
|
||||
// that the content size is unknown. https://godoc.org/github.com/badu/http#Response
|
||||
if res != nil && res.ContentLength > 0 {
|
||||
responseSizeSummary.Observe(float64(res.ContentLength))
|
||||
|
||||
if res != nil {
|
||||
res.Body = httpclient.CountBytesReader(res.Body, func(bytesRead int64) {
|
||||
responseSizeSummary.Observe(float64(bytesRead))
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package kvstore
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,6 +2,7 @@ package kvstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -88,7 +89,8 @@ func (kv *kvStoreSQL) Set(ctx context.Context, orgId int64, namespace string, ke
|
||||
// Del deletes an item from the store.
|
||||
func (kv *kvStoreSQL) Del(ctx context.Context, orgId int64, namespace string, key string) error {
|
||||
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
_, err := dbSession.Exec("DELETE FROM kv_store WHERE org_id=? and namespace=? and key=?", orgId, namespace, key)
|
||||
query := fmt.Sprintf("DELETE FROM kv_store WHERE org_id=? and namespace=? and %s=?", kv.sqlStore.Quote("key"))
|
||||
_, err := dbSession.Exec(query, orgId, namespace, key)
|
||||
return err
|
||||
})
|
||||
return err
|
||||
@@ -99,7 +101,7 @@ func (kv *kvStoreSQL) Del(ctx context.Context, orgId int64, namespace string, ke
|
||||
func (kv *kvStoreSQL) Keys(ctx context.Context, orgId int64, namespace string, keyPrefix string) ([]Key, error) {
|
||||
var keys []Key
|
||||
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
query := dbSession.Where("namespace = ?", namespace).And("\"key\" LIKE ?", keyPrefix+"%")
|
||||
query := dbSession.Where("namespace = ?", namespace).And(fmt.Sprintf("%s LIKE ?", kv.sqlStore.Quote("key")), keyPrefix+"%")
|
||||
if orgId != AllOrganizations {
|
||||
query.And("org_id = ?", orgId)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,14 @@ func Warnf(format string, v ...interface{}) {
|
||||
Root.Warn(message)
|
||||
}
|
||||
|
||||
func Debug(msg string, args ...interface{}) {
|
||||
Root.Debug(msg, args...)
|
||||
}
|
||||
|
||||
func Info(msg string, args ...interface{}) {
|
||||
Root.Info(msg, args...)
|
||||
}
|
||||
|
||||
func Error(msg string, args ...interface{}) {
|
||||
Root.Error(msg, args...)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (s *SocialBase) httpGet(client *http.Client, url string) (response httpGetR
|
||||
return
|
||||
}
|
||||
|
||||
log.Tracef("HTTP GET %s: %s %s", url, r.Status, string(response.Body))
|
||||
log.Debug("HTTP GET", "url", url, "status", r.Status, "response_body", string(response.Body))
|
||||
|
||||
err = nil
|
||||
return
|
||||
|
||||
@@ -49,7 +49,7 @@ func (pm *PluginManager) checkForUpdates() {
|
||||
pluginSlugs := pm.getAllExternalPluginSlugs()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to get plugins repo from grafana.com, %v", err.Error())
|
||||
log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
@@ -60,14 +60,14 @@ func (pm *PluginManager) checkForUpdates() {
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Tracef("Update check failed, reading response from grafana.com, %v", err.Error())
|
||||
log.Debug("Update check failed, reading response from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
gNetPlugins := []grafanaNetPlugin{}
|
||||
err = json.Unmarshal(body, &gNetPlugins)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to unmarshal plugin repo, reading response from grafana.com, %v", err.Error())
|
||||
log.Debug("Failed to unmarshal plugin repo, reading response from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (pm *PluginManager) checkForUpdates() {
|
||||
|
||||
resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/main/latest.json")
|
||||
if err != nil {
|
||||
log.Tracef("Failed to get latest.json repo from github.com: %v", err.Error())
|
||||
log.Debug("Failed to get latest.json repo from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
@@ -100,14 +100,14 @@ func (pm *PluginManager) checkForUpdates() {
|
||||
}()
|
||||
body, err = ioutil.ReadAll(resp2.Body)
|
||||
if err != nil {
|
||||
log.Tracef("Update check failed, reading response from github.com, %v", err.Error())
|
||||
log.Debug("Update check failed, reading response from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var latest gitHubLatest
|
||||
err = json.Unmarshal(body, &latest)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to unmarshal github.com latest, reading response from github.com: %v", err.Error())
|
||||
log.Debug("Failed to unmarshal github.com latest, reading response from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ func (ac *OSSAccessControlService) saveFixedRole(role accesscontrol.RoleDTO) {
|
||||
// needs to be increased. Hence, we don't overwrite a role with a
|
||||
// greater version.
|
||||
if storedRole.Version >= role.Version {
|
||||
log.Debugf("role %v has already been stored in a greater version, skipping registration", role.Name)
|
||||
log.Debug("the has already been stored in a greater version, skipping registration", "role", role.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (ac *OSSAccessControlService) assignFixedRole(role accesscontrol.RoleDTO, b
|
||||
if ok {
|
||||
for _, assignedRole := range assignments {
|
||||
if assignedRole == role.Name {
|
||||
log.Debugf("role %v has already been assigned to %v", role.Name, builtInRole)
|
||||
log.Debug("the role has already been assigned", "rolename", role.Name, "build_in_role", builtInRole)
|
||||
alreadyAssigned = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,60 +3,59 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func evaluatorScenario(json string, reducedValue float64, datapoints ...float64) bool {
|
||||
func evaluatorScenario(t *testing.T, json string, reducedValue float64, datapoints ...float64) bool {
|
||||
jsonModel, err := simplejson.NewJson([]byte(json))
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return evaluator.Eval(null.FloatFrom(reducedValue))
|
||||
}
|
||||
|
||||
func TestEvaluators(t *testing.T) {
|
||||
Convey("greater then", t, func() {
|
||||
So(evaluatorScenario(`{"type": "gt", "params": [1] }`, 3), ShouldBeTrue)
|
||||
So(evaluatorScenario(`{"type": "gt", "params": [3] }`, 1), ShouldBeFalse)
|
||||
t.Run("greater then", func(t *testing.T) {
|
||||
require.True(t, evaluatorScenario(t, `{"type": "gt", "params": [1] }`, 3))
|
||||
require.False(t, evaluatorScenario(t, `{"type": "gt", "params": [3] }`, 1))
|
||||
})
|
||||
|
||||
Convey("less then", t, func() {
|
||||
So(evaluatorScenario(`{"type": "lt", "params": [1] }`, 3), ShouldBeFalse)
|
||||
So(evaluatorScenario(`{"type": "lt", "params": [3] }`, 1), ShouldBeTrue)
|
||||
t.Run("less then", func(t *testing.T) {
|
||||
require.False(t, evaluatorScenario(t, `{"type": "lt", "params": [1] }`, 3))
|
||||
require.True(t, evaluatorScenario(t, `{"type": "lt", "params": [3] }`, 1))
|
||||
})
|
||||
|
||||
Convey("within_range", t, func() {
|
||||
So(evaluatorScenario(`{"type": "within_range", "params": [1, 100] }`, 3), ShouldBeTrue)
|
||||
So(evaluatorScenario(`{"type": "within_range", "params": [1, 100] }`, 300), ShouldBeFalse)
|
||||
So(evaluatorScenario(`{"type": "within_range", "params": [100, 1] }`, 3), ShouldBeTrue)
|
||||
So(evaluatorScenario(`{"type": "within_range", "params": [100, 1] }`, 300), ShouldBeFalse)
|
||||
t.Run("within_range", func(t *testing.T) {
|
||||
require.True(t, evaluatorScenario(t, `{"type": "within_range", "params": [1, 100] }`, 3))
|
||||
require.False(t, evaluatorScenario(t, `{"type": "within_range", "params": [1, 100] }`, 300))
|
||||
require.True(t, evaluatorScenario(t, `{"type": "within_range", "params": [100, 1] }`, 3))
|
||||
require.False(t, evaluatorScenario(t, `{"type": "within_range", "params": [100, 1] }`, 300))
|
||||
})
|
||||
|
||||
Convey("outside_range", t, func() {
|
||||
So(evaluatorScenario(`{"type": "outside_range", "params": [1, 100] }`, 1000), ShouldBeTrue)
|
||||
So(evaluatorScenario(`{"type": "outside_range", "params": [1, 100] }`, 50), ShouldBeFalse)
|
||||
So(evaluatorScenario(`{"type": "outside_range", "params": [100, 1] }`, 1000), ShouldBeTrue)
|
||||
So(evaluatorScenario(`{"type": "outside_range", "params": [100, 1] }`, 50), ShouldBeFalse)
|
||||
t.Run("outside_range", func(t *testing.T) {
|
||||
require.True(t, evaluatorScenario(t, `{"type": "outside_range", "params": [1, 100] }`, 1000))
|
||||
require.False(t, evaluatorScenario(t, `{"type": "outside_range", "params": [1, 100] }`, 50))
|
||||
require.True(t, evaluatorScenario(t, `{"type": "outside_range", "params": [100, 1] }`, 1000))
|
||||
require.False(t, evaluatorScenario(t, `{"type": "outside_range", "params": [100, 1] }`, 50))
|
||||
})
|
||||
|
||||
Convey("no_value", t, func() {
|
||||
Convey("should be false if series have values", func() {
|
||||
So(evaluatorScenario(`{"type": "no_value", "params": [] }`, 50), ShouldBeFalse)
|
||||
t.Run("no_value", func(t *testing.T) {
|
||||
t.Run("should be false if series have values", func(t *testing.T) {
|
||||
require.False(t, evaluatorScenario(t, `{"type": "no_value", "params": [] }`, 50))
|
||||
})
|
||||
|
||||
Convey("should be true when the series have no value", func() {
|
||||
t.Run("should be true when the series have no value", func(t *testing.T) {
|
||||
jsonModel, err := simplejson.NewJson([]byte(`{"type": "no_value", "params": [] }`))
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue)
|
||||
require.True(t, evaluator.Eval(null.FloatFromPtr(nil)))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQueryInterval(t *testing.T) {
|
||||
Convey("When evaluating query condition, regarding the interval value", t, func() {
|
||||
Convey("Can handle interval-calculation with no panel-min-interval and no datasource-min-interval", func() {
|
||||
t.Run("When evaluating query condition, regarding the interval value", func(t *testing.T) {
|
||||
t.Run("Can handle interval-calculation with no panel-min-interval and no datasource-min-interval", func(t *testing.T) {
|
||||
// no panel-min-interval in the queryModel
|
||||
queryModel := `{"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`
|
||||
|
||||
@@ -29,13 +30,13 @@ func TestQueryInterval(t *testing.T) {
|
||||
verifier := func(query plugins.DataSubQuery) {
|
||||
// 5minutes timerange = 300000milliseconds; default-resolution is 1500pixels,
|
||||
// so we should have 300000/1500 = 200milliseconds here
|
||||
So(query.IntervalMS, ShouldEqual, 200)
|
||||
So(query.MaxDataPoints, ShouldEqual, interval.DefaultRes)
|
||||
require.Equal(t, int64(200), query.IntervalMS)
|
||||
require.Equal(t, interval.DefaultRes, query.MaxDataPoints)
|
||||
}
|
||||
|
||||
applyScenario(timeRange, dataSourceJson, queryModel, verifier)
|
||||
applyScenario(t, timeRange, dataSourceJson, queryModel, verifier)
|
||||
})
|
||||
Convey("Can handle interval-calculation with panel-min-interval and no datasource-min-interval", func() {
|
||||
t.Run("Can handle interval-calculation with panel-min-interval and no datasource-min-interval", func(t *testing.T) {
|
||||
// panel-min-interval in the queryModel
|
||||
queryModel := `{"interval":"123s", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`
|
||||
|
||||
@@ -45,13 +46,13 @@ func TestQueryInterval(t *testing.T) {
|
||||
timeRange := "5m"
|
||||
|
||||
verifier := func(query plugins.DataSubQuery) {
|
||||
So(query.IntervalMS, ShouldEqual, 123000)
|
||||
So(query.MaxDataPoints, ShouldEqual, interval.DefaultRes)
|
||||
require.Equal(t, int64(123000), query.IntervalMS)
|
||||
require.Equal(t, interval.DefaultRes, query.MaxDataPoints)
|
||||
}
|
||||
|
||||
applyScenario(timeRange, dataSourceJson, queryModel, verifier)
|
||||
applyScenario(t, timeRange, dataSourceJson, queryModel, verifier)
|
||||
})
|
||||
Convey("Can handle interval-calculation with no panel-min-interval and datasource-min-interval", func() {
|
||||
t.Run("Can handle interval-calculation with no panel-min-interval and datasource-min-interval", func(t *testing.T) {
|
||||
// no panel-min-interval in the queryModel
|
||||
queryModel := `{"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`
|
||||
|
||||
@@ -59,18 +60,18 @@ func TestQueryInterval(t *testing.T) {
|
||||
dataSourceJson, err := simplejson.NewJson([]byte(`{
|
||||
"timeInterval": "71s"
|
||||
}`))
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
|
||||
timeRange := "5m"
|
||||
|
||||
verifier := func(query plugins.DataSubQuery) {
|
||||
So(query.IntervalMS, ShouldEqual, 71000)
|
||||
So(query.MaxDataPoints, ShouldEqual, interval.DefaultRes)
|
||||
require.Equal(t, int64(71000), query.IntervalMS)
|
||||
require.Equal(t, interval.DefaultRes, query.MaxDataPoints)
|
||||
}
|
||||
|
||||
applyScenario(timeRange, dataSourceJson, queryModel, verifier)
|
||||
applyScenario(t, timeRange, dataSourceJson, queryModel, verifier)
|
||||
})
|
||||
Convey("Can handle interval-calculation with both panel-min-interval and datasource-min-interval", func() {
|
||||
t.Run("Can handle interval-calculation with both panel-min-interval and datasource-min-interval", func(t *testing.T) {
|
||||
// panel-min-interval in the queryModel
|
||||
queryModel := `{"interval":"19s", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`
|
||||
|
||||
@@ -78,21 +79,21 @@ func TestQueryInterval(t *testing.T) {
|
||||
dataSourceJson, err := simplejson.NewJson([]byte(`{
|
||||
"timeInterval": "71s"
|
||||
}`))
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
|
||||
timeRange := "5m"
|
||||
|
||||
verifier := func(query plugins.DataSubQuery) {
|
||||
// when both panel-min-interval and datasource-min-interval exists,
|
||||
// panel-min-interval is used
|
||||
So(query.IntervalMS, ShouldEqual, 19000)
|
||||
So(query.MaxDataPoints, ShouldEqual, interval.DefaultRes)
|
||||
require.Equal(t, int64(19000), query.IntervalMS)
|
||||
require.Equal(t, interval.DefaultRes, query.MaxDataPoints)
|
||||
}
|
||||
|
||||
applyScenario(timeRange, dataSourceJson, queryModel, verifier)
|
||||
applyScenario(t, timeRange, dataSourceJson, queryModel, verifier)
|
||||
})
|
||||
|
||||
Convey("Can handle no min-interval, and very small time-ranges, where the default-min-interval=1ms applies", func() {
|
||||
t.Run("Can handle no min-interval, and very small time-ranges, where the default-min-interval=1ms applies", func(t *testing.T) {
|
||||
// no panel-min-interval in the queryModel
|
||||
queryModel := `{"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`
|
||||
|
||||
@@ -104,11 +105,11 @@ func TestQueryInterval(t *testing.T) {
|
||||
verifier := func(query plugins.DataSubQuery) {
|
||||
// no min-interval exists, the default-min-interval will be used,
|
||||
// and for such a short time-range this will cause the value to be 1millisecond.
|
||||
So(query.IntervalMS, ShouldEqual, 1)
|
||||
So(query.MaxDataPoints, ShouldEqual, interval.DefaultRes)
|
||||
require.Equal(t, int64(1), query.IntervalMS)
|
||||
require.Equal(t, interval.DefaultRes, query.MaxDataPoints)
|
||||
}
|
||||
|
||||
applyScenario(timeRange, dataSourceJson, queryModel, verifier)
|
||||
applyScenario(t, timeRange, dataSourceJson, queryModel, verifier)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -135,8 +136,8 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo *
|
||||
}
|
||||
|
||||
//nolint: staticcheck // plugins.DataResponse deprecated
|
||||
func applyScenario(timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query plugins.DataSubQuery)) {
|
||||
Convey("desc", func() {
|
||||
func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query plugins.DataSubQuery)) {
|
||||
t.Run("desc", func(t *testing.T) {
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, query *models.GetDataSourceQuery) error {
|
||||
query.Result = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData}
|
||||
return nil
|
||||
@@ -159,10 +160,10 @@ func applyScenario(timeRange string, dataSourceJsonData *simplejson.Json, queryM
|
||||
"reducer":{"type": "avg"},
|
||||
"evaluator":{"type": "gt", "params": [100]}
|
||||
}`))
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
|
||||
condition, err := newQueryCondition(jsonModel, 0)
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
|
||||
ctx.condition = condition
|
||||
|
||||
@@ -179,6 +180,6 @@ func applyScenario(timeRange string, dataSourceJsonData *simplejson.Json, queryM
|
||||
|
||||
_, err = condition.Eval(ctx.result, reqHandler)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/xorcare/pointer"
|
||||
)
|
||||
@@ -33,147 +33,165 @@ func newTimeSeriesPointsFromArgs(values ...float64) plugins.DataTimeSeriesPoints
|
||||
}
|
||||
|
||||
func TestQueryCondition(t *testing.T) {
|
||||
Convey("when evaluating query condition", t, func() {
|
||||
queryConditionScenario("Given avg() and > 100", func(ctx *queryConditionTestContext) {
|
||||
ctx.reducer = `{"type": "avg"}`
|
||||
ctx.evaluator = `{"type": "gt", "params": [100]}`
|
||||
setup := func() *queryConditionTestContext {
|
||||
ctx := &queryConditionTestContext{}
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, query *models.GetDataSourceQuery) error {
|
||||
query.Result = &models.DataSource{Id: 1, Type: "graphite"}
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("Can read query condition from json model", func() {
|
||||
_, err := ctx.exec()
|
||||
So(err, ShouldBeNil)
|
||||
ctx.reducer = `{"type":"avg"}`
|
||||
ctx.evaluator = `{"type":"gt","params":[100]}`
|
||||
ctx.result = &alerting.EvalContext{
|
||||
Ctx: context.Background(),
|
||||
Rule: &alerting.Rule{},
|
||||
RequestValidator: &validations.OSSPluginRequestValidator{},
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
So(ctx.condition.Query.From, ShouldEqual, "5m")
|
||||
So(ctx.condition.Query.To, ShouldEqual, "now")
|
||||
So(ctx.condition.Query.DatasourceID, ShouldEqual, 1)
|
||||
t.Run("Can read query condition from json model", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
_, err := ctx.exec(t)
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("Can read query reducer", func() {
|
||||
reducer := ctx.condition.Reducer
|
||||
So(reducer.Type, ShouldEqual, "avg")
|
||||
})
|
||||
require.Equal(t, "5m", ctx.condition.Query.From)
|
||||
require.Equal(t, "now", ctx.condition.Query.To)
|
||||
require.Equal(t, int64(1), ctx.condition.Query.DatasourceID)
|
||||
|
||||
Convey("Can read evaluator", func() {
|
||||
evaluator, ok := ctx.condition.Evaluator.(*thresholdEvaluator)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(evaluator.Type, ShouldEqual, "gt")
|
||||
})
|
||||
})
|
||||
t.Run("Can read query reducer", func(t *testing.T) {
|
||||
reducer := ctx.condition.Reducer
|
||||
require.Equal(t, "avg", reducer.Type)
|
||||
})
|
||||
|
||||
Convey("should fire when avg is above 100", func() {
|
||||
points := newTimeSeriesPointsFromArgs(120, 0)
|
||||
ctx.series = plugins.DataTimeSeriesSlice{plugins.DataTimeSeries{Name: "test1", Points: points}}
|
||||
cr, err := ctx.exec()
|
||||
t.Run("Can read evaluator", func(t *testing.T) {
|
||||
evaluator, ok := ctx.condition.Evaluator.(*thresholdEvaluator)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "gt", evaluator.Type)
|
||||
})
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
t.Run("should fire when avg is above 100", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
points := newTimeSeriesPointsFromArgs(120, 0)
|
||||
ctx.series = plugins.DataTimeSeriesSlice{plugins.DataTimeSeries{Name: "test1", Points: points}}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("should fire when avg is above 100 on dataframe", func() {
|
||||
ctx.frame = data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
||||
data.NewField("val", nil, []int64{120, 150}),
|
||||
)
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.Firing)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
t.Run("should fire when avg is above 100 on dataframe", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
ctx.frame = data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
||||
data.NewField("val", nil, []int64{120, 150}),
|
||||
)
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should not fire when avg is below 100", func() {
|
||||
points := newTimeSeriesPointsFromArgs(90, 0)
|
||||
ctx.series = plugins.DataTimeSeriesSlice{plugins.DataTimeSeries{Name: "test1", Points: points}}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.Firing)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeFalse)
|
||||
})
|
||||
t.Run("Should not fire when avg is below 100", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
points := newTimeSeriesPointsFromArgs(90, 0)
|
||||
ctx.series = plugins.DataTimeSeriesSlice{plugins.DataTimeSeries{Name: "test1", Points: points}}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should not fire when avg is below 100 on dataframe", func() {
|
||||
ctx.frame = data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
||||
data.NewField("val", nil, []int64{12, 47}),
|
||||
)
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.False(t, cr.Firing)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeFalse)
|
||||
})
|
||||
t.Run("Should not fire when avg is below 100 on dataframe", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
ctx.frame = data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
||||
data.NewField("val", nil, []int64{12, 47}),
|
||||
)
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should fire if only first series matches", func() {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs(120, 0)},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs(0, 0)},
|
||||
}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.False(t, cr.Firing)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
t.Run("Should fire if only first series matches", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs(120, 0)},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs(0, 0)},
|
||||
}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("No series", func() {
|
||||
Convey("Should set NoDataFound when condition is gt", func() {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.Firing)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeFalse)
|
||||
So(cr.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
t.Run("No series", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
t.Run("Should set NoDataFound when condition is gt", func(t *testing.T) {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should be firing when condition is no_value", func() {
|
||||
ctx.evaluator = `{"type": "no_value", "params": []}`
|
||||
ctx.series = plugins.DataTimeSeriesSlice{}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.False(t, cr.Firing)
|
||||
require.True(t, cr.NoDataFound)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
t.Run("Should be firing when condition is no_value", func(t *testing.T) {
|
||||
ctx.evaluator = `{"type": "no_value", "params": []}`
|
||||
ctx.series = plugins.DataTimeSeriesSlice{}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Empty series", func() {
|
||||
Convey("Should set Firing if eval match", func() {
|
||||
ctx.evaluator = `{"type": "no_value", "params": []}`
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
||||
}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.Firing)
|
||||
})
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
t.Run("Empty series", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
t.Run("Should set Firing if eval match", func(t *testing.T) {
|
||||
ctx.evaluator = `{"type": "no_value", "params": []}`
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
||||
}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should set NoDataFound both series are empty", func() {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs()},
|
||||
}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.Firing)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
t.Run("Should set NoDataFound both series are empty", func(t *testing.T) {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs()},
|
||||
}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should set NoDataFound both series contains null", func() {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: plugins.DataTimeSeriesPoints{plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: plugins.DataTimeSeriesPoints{plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}},
|
||||
}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.NoDataFound)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
t.Run("Should set NoDataFound both series contains null", func(t *testing.T) {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: plugins.DataTimeSeriesPoints{plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: plugins.DataTimeSeriesPoints{plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}},
|
||||
}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
Convey("Should not set NoDataFound if one series is empty", func() {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs(120, 0)},
|
||||
}
|
||||
cr, err := ctx.exec()
|
||||
require.Nil(t, err)
|
||||
require.True(t, cr.NoDataFound)
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.NoDataFound, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
t.Run("Should not set NoDataFound if one series is empty", func(t *testing.T) {
|
||||
ctx.series = plugins.DataTimeSeriesSlice{
|
||||
plugins.DataTimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
||||
plugins.DataTimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs(120, 0)},
|
||||
}
|
||||
cr, err := ctx.exec(t)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.False(t, cr.NoDataFound)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -187,10 +205,8 @@ type queryConditionTestContext struct {
|
||||
condition *QueryCondition
|
||||
}
|
||||
|
||||
type queryConditionScenarioFunc func(c *queryConditionTestContext)
|
||||
|
||||
//nolint: staticcheck // plugins.DataPlugin deprecated
|
||||
func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
|
||||
func (ctx *queryConditionTestContext) exec(t *testing.T) (*alerting.ConditionResult, error) {
|
||||
jsonModel, err := simplejson.NewJson([]byte(`{
|
||||
"type": "query",
|
||||
"query": {
|
||||
@@ -201,10 +217,10 @@ func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error)
|
||||
"reducer":` + ctx.reducer + `,
|
||||
"evaluator":` + ctx.evaluator + `
|
||||
}`))
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
|
||||
condition, err := newQueryCondition(jsonModel, 0)
|
||||
So(err, ShouldBeNil)
|
||||
require.Nil(t, err)
|
||||
|
||||
ctx.condition = condition
|
||||
|
||||
@@ -239,24 +255,6 @@ func (rh fakeReqHandler) HandleRequest(context.Context, *models.DataSource, plug
|
||||
return rh.response, nil
|
||||
}
|
||||
|
||||
func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, query *models.GetDataSourceQuery) error {
|
||||
query.Result = &models.DataSource{Id: 1, Type: "graphite"}
|
||||
return nil
|
||||
})
|
||||
|
||||
ctx := &queryConditionTestContext{}
|
||||
ctx.result = &alerting.EvalContext{
|
||||
Ctx: context.Background(),
|
||||
Rule: &alerting.Rule{},
|
||||
RequestValidator: &validations.OSSPluginRequestValidator{},
|
||||
}
|
||||
|
||||
fn(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrameToSeriesSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -4,183 +4,103 @@ import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSimpleReducer(t *testing.T) {
|
||||
Convey("Test simple reducer by calculating", t, func() {
|
||||
Convey("sum", func() {
|
||||
result := testReducer("sum", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(6))
|
||||
})
|
||||
t.Run("sum", func(t *testing.T) {
|
||||
result := testReducer("sum", 1, 2, 3)
|
||||
require.Equal(t, float64(6), result)
|
||||
})
|
||||
|
||||
Convey("min", func() {
|
||||
result := testReducer("min", 3, 2, 1)
|
||||
So(result, ShouldEqual, float64(1))
|
||||
})
|
||||
t.Run("min", func(t *testing.T) {
|
||||
result := testReducer("min", 3, 2, 1)
|
||||
require.Equal(t, float64(1), result)
|
||||
})
|
||||
|
||||
Convey("max", func() {
|
||||
result := testReducer("max", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
t.Run("max", func(t *testing.T) {
|
||||
result := testReducer("max", 1, 2, 3)
|
||||
require.Equal(t, float64(3), result)
|
||||
})
|
||||
|
||||
Convey("count", func() {
|
||||
result := testReducer("count", 1, 2, 3000)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
t.Run("count", func(t *testing.T) {
|
||||
result := testReducer("count", 1, 2, 3000)
|
||||
require.Equal(t, float64(3), result)
|
||||
})
|
||||
|
||||
Convey("last", func() {
|
||||
result := testReducer("last", 1, 2, 3000)
|
||||
So(result, ShouldEqual, float64(3000))
|
||||
})
|
||||
t.Run("last", func(t *testing.T) {
|
||||
result := testReducer("last", 1, 2, 3000)
|
||||
require.Equal(t, float64(3000), result)
|
||||
})
|
||||
|
||||
Convey("median odd amount of numbers", func() {
|
||||
result := testReducer("median", 1, 2, 3000)
|
||||
So(result, ShouldEqual, float64(2))
|
||||
})
|
||||
t.Run("median odd amount of numbers", func(t *testing.T) {
|
||||
result := testReducer("median", 1, 2, 3000)
|
||||
require.Equal(t, float64(2), result)
|
||||
})
|
||||
|
||||
Convey("median even amount of numbers", func() {
|
||||
result := testReducer("median", 1, 2, 4, 3000)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
t.Run("median even amount of numbers", func(t *testing.T) {
|
||||
result := testReducer("median", 1, 2, 4, 3000)
|
||||
require.Equal(t, float64(3), result)
|
||||
})
|
||||
|
||||
Convey("median with one values", func() {
|
||||
result := testReducer("median", 1)
|
||||
So(result, ShouldEqual, float64(1))
|
||||
})
|
||||
t.Run("median with one values", func(t *testing.T) {
|
||||
result := testReducer("median", 1)
|
||||
require.Equal(t, float64(1), result)
|
||||
})
|
||||
|
||||
Convey("median should ignore null values", func() {
|
||||
reducer := newSimpleReducer("median")
|
||||
t.Run("median should ignore null values", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("median")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(3)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(float64(1)), null.FloatFrom(4)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(float64(2)), null.FloatFrom(5)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(float64(3)), null.FloatFrom(6)})
|
||||
|
||||
result := reducer.Reduce(series)
|
||||
require.Equal(t, true, result.Valid)
|
||||
require.Equal(t, float64(2), result.Float64)
|
||||
})
|
||||
|
||||
t.Run("avg", func(t *testing.T) {
|
||||
result := testReducer("avg", 1, 2, 3)
|
||||
require.Equal(t, float64(2), result)
|
||||
})
|
||||
|
||||
t.Run("avg with only nulls", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("avg")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
require.Equal(t, false, reducer.Reduce(series).Valid)
|
||||
})
|
||||
|
||||
t.Run("count_non_null", func(t *testing.T) {
|
||||
t.Run("with null values and real values", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("count_non_null")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(3)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(float64(1)), null.FloatFrom(4)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(float64(2)), null.FloatFrom(5)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(float64(3)), null.FloatFrom(6)})
|
||||
|
||||
result := reducer.Reduce(series)
|
||||
So(result.Valid, ShouldEqual, true)
|
||||
So(result.Float64, ShouldEqual, float64(2))
|
||||
})
|
||||
|
||||
Convey("avg", func() {
|
||||
result := testReducer("avg", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(2))
|
||||
})
|
||||
|
||||
Convey("avg with only nulls", func() {
|
||||
reducer := newSimpleReducer("avg")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("count_non_null", func() {
|
||||
Convey("with null values and real values", func() {
|
||||
reducer := newSimpleReducer("count_non_null")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(3)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(4)})
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, true)
|
||||
So(reducer.Reduce(series).Float64, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("with null values", func() {
|
||||
reducer := newSimpleReducer("count_non_null")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("avg of number values and null values should ignore nulls", func() {
|
||||
reducer := newSimpleReducer("avg")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(3)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(3)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(4)})
|
||||
|
||||
So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
|
||||
require.Equal(t, true, reducer.Reduce(series).Valid)
|
||||
require.Equal(t, 2.0, reducer.Reduce(series).Float64)
|
||||
})
|
||||
|
||||
// diff function Test Suite
|
||||
Convey("diff of one positive point", func() {
|
||||
result := testReducer("diff", 30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
|
||||
Convey("diff of one negative point", func() {
|
||||
result := testReducer("diff", -30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
|
||||
Convey("diff of two positive points[1]", func() {
|
||||
result := testReducer("diff", 30, 40)
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
|
||||
Convey("diff of two positive points[2]", func() {
|
||||
result := testReducer("diff", 30, 20)
|
||||
So(result, ShouldEqual, float64(-10))
|
||||
})
|
||||
|
||||
Convey("diff of two negative points[1]", func() {
|
||||
result := testReducer("diff", -30, -40)
|
||||
So(result, ShouldEqual, float64(-10))
|
||||
})
|
||||
|
||||
Convey("diff of two negative points[2]", func() {
|
||||
result := testReducer("diff", -30, -10)
|
||||
So(result, ShouldEqual, float64(20))
|
||||
})
|
||||
|
||||
Convey("diff of one positive and one negative point", func() {
|
||||
result := testReducer("diff", 30, -40)
|
||||
So(result, ShouldEqual, float64(-70))
|
||||
})
|
||||
|
||||
Convey("diff of one negative and one positive point", func() {
|
||||
result := testReducer("diff", -30, 40)
|
||||
So(result, ShouldEqual, float64(70))
|
||||
})
|
||||
|
||||
Convey("diff of three positive points", func() {
|
||||
result := testReducer("diff", 30, 40, 50)
|
||||
So(result, ShouldEqual, float64(20))
|
||||
})
|
||||
|
||||
Convey("diff of three negative points", func() {
|
||||
result := testReducer("diff", -30, -40, -50)
|
||||
So(result, ShouldEqual, float64(-20))
|
||||
})
|
||||
|
||||
Convey("diff with only nulls", func() {
|
||||
reducer := newSimpleReducer("diff")
|
||||
t.Run("with null values", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("count_non_null")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
@@ -188,212 +108,289 @@ func TestSimpleReducer(t *testing.T) {
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
require.Equal(t, false, reducer.Reduce(series).Valid)
|
||||
})
|
||||
})
|
||||
|
||||
// diff_abs function Test Suite
|
||||
Convey("diff_abs of one positive point", func() {
|
||||
result := testReducer("diff_abs", 30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
t.Run("avg of number values and null values should ignore nulls", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("avg")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
Convey("diff_abs of one negative point", func() {
|
||||
result := testReducer("diff_abs", -30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(3)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFrom(3), null.FloatFrom(4)})
|
||||
|
||||
Convey("diff_abs of two positive points[1]", func() {
|
||||
result := testReducer("diff_abs", 30, 40)
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
require.Equal(t, float64(3), reducer.Reduce(series).Float64)
|
||||
})
|
||||
|
||||
Convey("diff_abs of two positive points[2]", func() {
|
||||
result := testReducer("diff_abs", 30, 20)
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
// diff function Test Suite
|
||||
t.Run("diff of one positive point", func(t *testing.T) {
|
||||
result := testReducer("diff", 30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs of two negative points[1]", func() {
|
||||
result := testReducer("diff_abs", -30, -40)
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
t.Run("diff of one negative point", func(t *testing.T) {
|
||||
result := testReducer("diff", -30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs of two negative points[2]", func() {
|
||||
result := testReducer("diff_abs", -30, -10)
|
||||
So(result, ShouldEqual, float64(20))
|
||||
})
|
||||
t.Run("diff of two positive points[1]", func(t *testing.T) {
|
||||
result := testReducer("diff", 30, 40)
|
||||
require.Equal(t, float64(10), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs of one positive and one negative point", func() {
|
||||
result := testReducer("diff_abs", 30, -40)
|
||||
So(result, ShouldEqual, float64(70))
|
||||
})
|
||||
t.Run("diff of two positive points[2]", func(t *testing.T) {
|
||||
result := testReducer("diff", 30, 20)
|
||||
require.Equal(t, float64(-10), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs of one negative and one positive point", func() {
|
||||
result := testReducer("diff_abs", -30, 40)
|
||||
So(result, ShouldEqual, float64(70))
|
||||
})
|
||||
t.Run("diff of two negative points[1]", func(t *testing.T) {
|
||||
result := testReducer("diff", -30, -40)
|
||||
require.Equal(t, float64(-10), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs of three positive points", func() {
|
||||
result := testReducer("diff_abs", 30, 40, 50)
|
||||
So(result, ShouldEqual, float64(20))
|
||||
})
|
||||
t.Run("diff of two negative points[2]", func(t *testing.T) {
|
||||
result := testReducer("diff", -30, -10)
|
||||
require.Equal(t, float64(20), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs of three negative points", func() {
|
||||
result := testReducer("diff_abs", -30, -40, -50)
|
||||
So(result, ShouldEqual, float64(20))
|
||||
})
|
||||
t.Run("diff of one positive and one negative point", func(t *testing.T) {
|
||||
result := testReducer("diff", 30, -40)
|
||||
require.Equal(t, float64(-70), result)
|
||||
})
|
||||
|
||||
Convey("diff_abs with only nulls", func() {
|
||||
reducer := newSimpleReducer("diff_abs")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
t.Run("diff of one negative and one positive point", func(t *testing.T) {
|
||||
result := testReducer("diff", -30, 40)
|
||||
require.Equal(t, float64(70), result)
|
||||
})
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
t.Run("diff of three positive points", func(t *testing.T) {
|
||||
result := testReducer("diff", 30, 40, 50)
|
||||
require.Equal(t, float64(20), result)
|
||||
})
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
t.Run("diff of three negative points", func(t *testing.T) {
|
||||
result := testReducer("diff", -30, -40, -50)
|
||||
require.Equal(t, float64(-20), result)
|
||||
})
|
||||
|
||||
// percent_diff function Test Suite
|
||||
Convey("percent_diff of one positive point", func() {
|
||||
result := testReducer("percent_diff", 30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
t.Run("diff with only nulls", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("diff")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
Convey("percent_diff of one negative point", func() {
|
||||
result := testReducer("percent_diff", -30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
|
||||
Convey("percent_diff of two positive points[1]", func() {
|
||||
result := testReducer("percent_diff", 30, 40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
require.Equal(t, false, reducer.Reduce(series).Valid)
|
||||
})
|
||||
|
||||
Convey("percent_diff of two positive points[2]", func() {
|
||||
result := testReducer("percent_diff", 30, 20)
|
||||
So(result, ShouldEqual, float64(-33.33333333333333))
|
||||
})
|
||||
// diff_abs function Test Suite
|
||||
t.Run("diff_abs of one positive point", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", 30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff of two negative points[1]", func() {
|
||||
result := testReducer("percent_diff", -30, -40)
|
||||
So(result, ShouldEqual, float64(-33.33333333333333))
|
||||
})
|
||||
t.Run("diff_abs of one negative point", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", -30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff of two negative points[2]", func() {
|
||||
result := testReducer("percent_diff", -30, -10)
|
||||
So(result, ShouldEqual, float64(66.66666666666666))
|
||||
})
|
||||
t.Run("diff_abs of two positive points[1]", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", 30, 40)
|
||||
require.Equal(t, float64(10), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff of one positive and one negative point", func() {
|
||||
result := testReducer("percent_diff", 30, -40)
|
||||
So(result, ShouldEqual, float64(-233.33333333333334))
|
||||
})
|
||||
t.Run("diff_abs of two positive points[2]", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", 30, 20)
|
||||
require.Equal(t, float64(10), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff of one negative and one positive point", func() {
|
||||
result := testReducer("percent_diff", -30, 40)
|
||||
So(result, ShouldEqual, float64(233.33333333333334))
|
||||
})
|
||||
t.Run("diff_abs of two negative points[1]", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", -30, -40)
|
||||
require.Equal(t, float64(10), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff of three positive points", func() {
|
||||
result := testReducer("percent_diff", 30, 40, 50)
|
||||
So(result, ShouldEqual, float64(66.66666666666666))
|
||||
})
|
||||
t.Run("diff_abs of two negative points[2]", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", -30, -10)
|
||||
require.Equal(t, float64(20), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff of three negative points", func() {
|
||||
result := testReducer("percent_diff", -30, -40, -50)
|
||||
So(result, ShouldEqual, float64(-66.66666666666666))
|
||||
})
|
||||
t.Run("diff_abs of one positive and one negative point", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", 30, -40)
|
||||
require.Equal(t, float64(70), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff with only nulls", func() {
|
||||
reducer := newSimpleReducer("percent_diff")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
t.Run("diff_abs of one negative and one positive point", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", -30, 40)
|
||||
require.Equal(t, float64(70), result)
|
||||
})
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
t.Run("diff_abs of three positive points", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", 30, 40, 50)
|
||||
require.Equal(t, float64(20), result)
|
||||
})
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
t.Run("diff_abs of three negative points", func(t *testing.T) {
|
||||
result := testReducer("diff_abs", -30, -40, -50)
|
||||
require.Equal(t, float64(20), result)
|
||||
})
|
||||
|
||||
// percent_diff_abs function Test Suite
|
||||
Convey("percent_diff_abs_abs of one positive point", func() {
|
||||
result := testReducer("percent_diff_abs", 30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
t.Run("diff_abs with only nulls", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("diff_abs")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
Convey("percent_diff_abs of one negative point", func() {
|
||||
result := testReducer("percent_diff_abs", -30)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
|
||||
Convey("percent_diff_abs of two positive points[1]", func() {
|
||||
result := testReducer("percent_diff_abs", 30, 40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
require.Equal(t, false, reducer.Reduce(series).Valid)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of two positive points[2]", func() {
|
||||
result := testReducer("percent_diff_abs", 30, 20)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
// percent_diff function Test Suite
|
||||
t.Run("percent_diff of one positive point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of two negative points[1]", func() {
|
||||
result := testReducer("percent_diff_abs", -30, -40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
t.Run("percent_diff of one negative point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", -30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of two negative points[2]", func() {
|
||||
result := testReducer("percent_diff_abs", -30, -10)
|
||||
So(result, ShouldEqual, float64(66.66666666666666))
|
||||
})
|
||||
t.Run("percent_diff of two positive points[1]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30, 40)
|
||||
require.Equal(t, float64(33.33333333333333), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of one positive and one negative point", func() {
|
||||
result := testReducer("percent_diff_abs", 30, -40)
|
||||
So(result, ShouldEqual, float64(233.33333333333334))
|
||||
})
|
||||
t.Run("percent_diff of two positive points[2]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30, 20)
|
||||
require.Equal(t, float64(-33.33333333333333), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of one negative and one positive point", func() {
|
||||
result := testReducer("percent_diff_abs", -30, 40)
|
||||
So(result, ShouldEqual, float64(233.33333333333334))
|
||||
})
|
||||
t.Run("percent_diff of two negative points[1]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", -30, -40)
|
||||
require.Equal(t, float64(-33.33333333333333), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of three positive points", func() {
|
||||
result := testReducer("percent_diff_abs", 30, 40, 50)
|
||||
So(result, ShouldEqual, float64(66.66666666666666))
|
||||
})
|
||||
t.Run("percent_diff of two negative points[2]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", -30, -10)
|
||||
require.Equal(t, float64(66.66666666666666), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs of three negative points", func() {
|
||||
result := testReducer("percent_diff_abs", -30, -40, -50)
|
||||
So(result, ShouldEqual, float64(66.66666666666666))
|
||||
})
|
||||
t.Run("percent_diff of one positive and one negative point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30, -40)
|
||||
require.Equal(t, float64(-233.33333333333334), result)
|
||||
})
|
||||
|
||||
Convey("percent_diff_abs with only nulls", func() {
|
||||
reducer := newSimpleReducer("percent_diff_abs")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
t.Run("percent_diff of one negative and one positive point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", -30, 40)
|
||||
require.Equal(t, float64(233.33333333333334), result)
|
||||
})
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
t.Run("percent_diff of three positive points", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30, 40, 50)
|
||||
require.Equal(t, float64(66.66666666666666), result)
|
||||
})
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
t.Run("percent_diff of three negative points", func(t *testing.T) {
|
||||
result := testReducer("percent_diff", -30, -40, -50)
|
||||
require.Equal(t, float64(-66.66666666666666), result)
|
||||
})
|
||||
|
||||
Convey("min should work with NaNs", func() {
|
||||
result := testReducer("min", math.NaN(), math.NaN(), math.NaN())
|
||||
So(result, ShouldEqual, float64(0))
|
||||
})
|
||||
t.Run("percent_diff with only nulls", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("percent_diff")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
Convey("isValid should treat NaN as invalid", func() {
|
||||
result := isValid(null.FloatFrom(math.NaN()))
|
||||
So(result, ShouldBeFalse)
|
||||
})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
|
||||
Convey("isValid should treat invalid null.Float as invalid", func() {
|
||||
result := isValid(null.FloatFromPtr(nil))
|
||||
So(result, ShouldBeFalse)
|
||||
})
|
||||
require.Equal(t, false, reducer.Reduce(series).Valid)
|
||||
})
|
||||
|
||||
// percent_diff_abs function Test Suite
|
||||
t.Run("percent_diff_abs_abs of one positive point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", 30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of one negative point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", -30)
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of two positive points[1]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", 30, 40)
|
||||
require.Equal(t, float64(33.33333333333333), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of two positive points[2]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", 30, 20)
|
||||
require.Equal(t, float64(33.33333333333333), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of two negative points[1]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", -30, -40)
|
||||
require.Equal(t, float64(33.33333333333333), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of two negative points[2]", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", -30, -10)
|
||||
require.Equal(t, float64(66.66666666666666), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of one positive and one negative point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", 30, -40)
|
||||
require.Equal(t, float64(233.33333333333334), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of one negative and one positive point", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", -30, 40)
|
||||
require.Equal(t, float64(233.33333333333334), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of three positive points", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", 30, 40, 50)
|
||||
require.Equal(t, float64(66.66666666666666), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs of three negative points", func(t *testing.T) {
|
||||
result := testReducer("percent_diff_abs", -30, -40, -50)
|
||||
require.Equal(t, float64(66.66666666666666), result)
|
||||
})
|
||||
|
||||
t.Run("percent_diff_abs with only nulls", func(t *testing.T) {
|
||||
reducer := newSimpleReducer("percent_diff_abs")
|
||||
series := plugins.DataTimeSeries{
|
||||
Name: "test time series",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(1)})
|
||||
series.Points = append(series.Points, plugins.DataTimePoint{null.FloatFromPtr(nil), null.FloatFrom(2)})
|
||||
|
||||
require.Equal(t, false, reducer.Reduce(series).Valid)
|
||||
})
|
||||
|
||||
t.Run("min should work with NaNs", func(t *testing.T) {
|
||||
result := testReducer("min", math.NaN(), math.NaN(), math.NaN())
|
||||
require.Equal(t, float64(0), result)
|
||||
})
|
||||
|
||||
t.Run("isValid should treat NaN as invalid", func(t *testing.T) {
|
||||
result := isValid(null.FloatFrom(math.NaN()))
|
||||
require.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("isValid should treat invalid null.Float as invalid", func(t *testing.T) {
|
||||
result := isValid(null.FloatFromPtr(nil))
|
||||
require.False(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,40 +15,39 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEngineTimeouts(t *testing.T) {
|
||||
Convey("Alerting engine timeout tests", t, func() {
|
||||
usMock := &usagestats.UsageStatsMock{T: t}
|
||||
engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg())
|
||||
setting.AlertingNotificationTimeout = 30 * time.Second
|
||||
setting.AlertingMaxAttempts = 3
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
job := &Job{running: true, Rule: &Rule{}}
|
||||
usMock := &usagestats.UsageStatsMock{T: t}
|
||||
engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg())
|
||||
setting.AlertingNotificationTimeout = 30 * time.Second
|
||||
setting.AlertingMaxAttempts = 3
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
job := &Job{running: true, Rule: &Rule{}}
|
||||
|
||||
Convey("Should trigger as many retries as needed", func() {
|
||||
Convey("pended alert for datasource -> result handler should be worked", func() {
|
||||
// reduce alert timeout to test quickly
|
||||
setting.AlertingEvaluationTimeout = 30 * time.Second
|
||||
transportTimeoutInterval := 2 * time.Second
|
||||
serverBusySleepDuration := 1 * time.Second
|
||||
t.Run("Should trigger as many retries as needed", func(t *testing.T) {
|
||||
t.Run("pended alert for datasource -> result handler should be worked", func(t *testing.T) {
|
||||
// reduce alert timeout to test quickly
|
||||
setting.AlertingEvaluationTimeout = 30 * time.Second
|
||||
transportTimeoutInterval := 2 * time.Second
|
||||
serverBusySleepDuration := 1 * time.Second
|
||||
|
||||
evalHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
|
||||
resultHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
|
||||
engine.evalHandler = evalHandler
|
||||
engine.resultHandler = resultHandler
|
||||
evalHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
|
||||
resultHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
|
||||
engine.evalHandler = evalHandler
|
||||
engine.resultHandler = resultHandler
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
So(err, ShouldBeNil)
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
require.Nil(t, err)
|
||||
|
||||
So(evalHandler.EvalSucceed, ShouldEqual, true)
|
||||
So(resultHandler.ResultHandleSucceed, ShouldEqual, true)
|
||||
require.Equal(t, true, evalHandler.EvalSucceed)
|
||||
require.Equal(t, true, resultHandler.ResultHandleSucceed)
|
||||
|
||||
// initialize for other tests.
|
||||
setting.AlertingEvaluationTimeout = 2 * time.Second
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
})
|
||||
// initialize for other tests.
|
||||
setting.AlertingEvaluationTimeout = 2 * time.Second
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type FakeEvalHandler struct {
|
||||
@@ -42,112 +43,110 @@ func (handler *FakeResultHandler) handle(evalContext *EvalContext) error {
|
||||
}
|
||||
|
||||
func TestEngineProcessJob(t *testing.T) {
|
||||
Convey("Alerting engine job processing", t, func() {
|
||||
bus := bus.New()
|
||||
usMock := &usagestats.UsageStatsMock{T: t}
|
||||
engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg())
|
||||
setting.AlertingEvaluationTimeout = 30 * time.Second
|
||||
setting.AlertingNotificationTimeout = 30 * time.Second
|
||||
setting.AlertingMaxAttempts = 3
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
job := &Job{running: true, Rule: &Rule{}}
|
||||
bus := bus.New()
|
||||
usMock := &usagestats.UsageStatsMock{T: t}
|
||||
engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg())
|
||||
setting.AlertingEvaluationTimeout = 30 * time.Second
|
||||
setting.AlertingNotificationTimeout = 30 * time.Second
|
||||
setting.AlertingMaxAttempts = 3
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
job := &Job{running: true, Rule: &Rule{}}
|
||||
|
||||
Convey("Should register usage metrics func", func() {
|
||||
bus.AddHandler(func(q *models.GetAllAlertsQuery) error {
|
||||
settings, err := simplejson.NewJson([]byte(`{"conditions": [{"query": { "datasourceId": 1}}]}`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.Result = []*models.Alert{{Settings: settings}}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler(func(q *models.GetDataSourceQuery) error {
|
||||
q.Result = &models.DataSource{Id: 1, Type: models.DS_PROMETHEUS}
|
||||
return nil
|
||||
})
|
||||
|
||||
report, err := usMock.GetUsageReport(context.Background())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(report.Metrics["stats.alerting.ds.prometheus.count"], ShouldEqual, 1)
|
||||
So(report.Metrics["stats.alerting.ds.other.count"], ShouldEqual, 0)
|
||||
t.Run("Should register usage metrics func", func(t *testing.T) {
|
||||
bus.AddHandler(func(q *models.GetAllAlertsQuery) error {
|
||||
settings, err := simplejson.NewJson([]byte(`{"conditions": [{"query": { "datasourceId": 1}}]}`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.Result = []*models.Alert{{Settings: settings}}
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("Should trigger retry if needed", func() {
|
||||
Convey("error + not last attempt -> retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
bus.AddHandler(func(q *models.GetDataSourceQuery) error {
|
||||
q.Result = &models.DataSource{Id: 1, Type: models.DS_PROMETHEUS}
|
||||
return nil
|
||||
})
|
||||
|
||||
for i := 1; i < setting.AlertingMaxAttempts; i++ {
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, setting.AlertingMaxAttempts)
|
||||
report, err := usMock.GetUsageReport(context.Background())
|
||||
require.Nil(t, err)
|
||||
|
||||
engine.processJob(i, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
require.Equal(t, 1, report.Metrics["stats.alerting.ds.prometheus.count"])
|
||||
require.Equal(t, 0, report.Metrics["stats.alerting.ds.other.count"])
|
||||
})
|
||||
|
||||
So(nextAttemptID, ShouldEqual, i+1)
|
||||
So(more, ShouldEqual, true)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
t.Run("Should trigger retry if needed", func(t *testing.T) {
|
||||
t.Run("error + not last attempt -> retry", func(t *testing.T) {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
|
||||
Convey("error + last attempt -> no retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
for i := 1; i < setting.AlertingMaxAttempts; i++ {
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, setting.AlertingMaxAttempts)
|
||||
|
||||
engine.processJob(setting.AlertingMaxAttempts, attemptChan, cancelChan, job)
|
||||
engine.processJob(i, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, 0)
|
||||
So(more, ShouldEqual, false)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("no error -> no retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(1)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, setting.AlertingMaxAttempts)
|
||||
|
||||
engine.processJob(1, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, 0)
|
||||
So(more, ShouldEqual, false)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
})
|
||||
require.Equal(t, i+1, nextAttemptID)
|
||||
require.Equal(t, true, more)
|
||||
require.NotNil(t, <-cancelChan)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Should trigger as many retries as needed", func() {
|
||||
Convey("never success -> max retries number", func() {
|
||||
expectedAttempts := setting.AlertingMaxAttempts
|
||||
evalHandler := NewFakeEvalHandler(0)
|
||||
engine.evalHandler = evalHandler
|
||||
t.Run("error + last attempt -> no retry", func(t *testing.T) {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, setting.AlertingMaxAttempts)
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
So(err, ShouldBeNil)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
engine.processJob(setting.AlertingMaxAttempts, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
Convey("always success -> never retry", func() {
|
||||
expectedAttempts := 1
|
||||
evalHandler := NewFakeEvalHandler(1)
|
||||
engine.evalHandler = evalHandler
|
||||
require.Equal(t, 0, nextAttemptID)
|
||||
require.Equal(t, false, more)
|
||||
require.NotNil(t, <-cancelChan)
|
||||
})
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
So(err, ShouldBeNil)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
t.Run("no error -> no retry", func(t *testing.T) {
|
||||
engine.evalHandler = NewFakeEvalHandler(1)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, setting.AlertingMaxAttempts)
|
||||
|
||||
Convey("some errors before success -> some retries", func() {
|
||||
expectedAttempts := int(math.Ceil(float64(setting.AlertingMaxAttempts) / 2))
|
||||
evalHandler := NewFakeEvalHandler(expectedAttempts)
|
||||
engine.evalHandler = evalHandler
|
||||
engine.processJob(1, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
So(err, ShouldBeNil)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
require.Equal(t, 0, nextAttemptID)
|
||||
require.Equal(t, false, more)
|
||||
require.NotNil(t, <-cancelChan)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should trigger as many retries as needed", func(t *testing.T) {
|
||||
t.Run("never success -> max retries number", func(t *testing.T) {
|
||||
expectedAttempts := setting.AlertingMaxAttempts
|
||||
evalHandler := NewFakeEvalHandler(0)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, expectedAttempts, evalHandler.CallNb)
|
||||
})
|
||||
|
||||
t.Run("always success -> never retry", func(t *testing.T) {
|
||||
expectedAttempts := 1
|
||||
evalHandler := NewFakeEvalHandler(1)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, expectedAttempts, evalHandler.CallNb)
|
||||
})
|
||||
|
||||
t.Run("some errors before success -> some retries", func(t *testing.T) {
|
||||
expectedAttempts := int(math.Ceil(float64(setting.AlertingMaxAttempts) / 2))
|
||||
evalHandler := NewFakeEvalHandler(expectedAttempts)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
err := engine.processJobWithRetry(context.TODO(), job)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, expectedAttempts, evalHandler.CallNb)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/validations"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type conditionStub struct {
|
||||
@@ -22,188 +22,186 @@ func (c *conditionStub) Eval(context *EvalContext, reqHandler plugins.DataReques
|
||||
}
|
||||
|
||||
func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
Convey("Test alert evaluation handler", t, func() {
|
||||
handler := NewEvalHandler(nil)
|
||||
handler := NewEvalHandler(nil)
|
||||
|
||||
Convey("Show return triggered with single passing condition", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{&conditionStub{
|
||||
firing: true,
|
||||
}},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return triggered with single passing condition", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{&conditionStub{
|
||||
firing: true,
|
||||
}},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, true, context.Firing)
|
||||
require.Equal(t, "true = true", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return triggered with single passing condition2", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return triggered with single passing condition2", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, true, context.Firing)
|
||||
require.Equal(t, "true = true", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return false with not passing asdf", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return false with not passing asdf", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, false, context.Firing)
|
||||
require.Equal(t, "[true AND false] = false", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return true if any of the condition is passing with OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return true if any of the condition is passing with OR operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, true, context.Firing)
|
||||
require.Equal(t, "[true OR false] = true", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return false if any of the condition is failing with AND operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return false if any of the condition is failing with AND operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, false, context.Firing)
|
||||
require.Equal(t, "[true AND false] = false", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return true if one condition is failing with nested OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return true if one condition is failing with nested OR operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, true, context.Firing)
|
||||
require.Equal(t, "[[true AND true] OR false] = true", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return false if one condition is passing with nested OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return false if one condition is passing with nested OR operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, false, context.Firing)
|
||||
require.Equal(t, "[[true AND false] OR false] = false", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return false if a condition is failing with nested AND operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return false if a condition is failing with nested AND operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, false, context.Firing)
|
||||
require.Equal(t, "[[true AND false] AND true] = false", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Show return true if a condition is passing with nested OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: true, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Show return true if a condition is passing with nested OR operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: true, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, true, context.Firing)
|
||||
require.Equal(t, "[[true OR false] OR true] = true", context.ConditionEvals)
|
||||
})
|
||||
|
||||
Convey("Should return false if no condition is firing using OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Should return false if no condition is firing using OR operator", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, false, context.Firing)
|
||||
require.Equal(t, "[[false OR false] OR false] = false", context.ConditionEvals)
|
||||
})
|
||||
|
||||
// FIXME: What should the actual test case name be here?
|
||||
Convey("Should not return NoDataFound if all conditions have data and using OR", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
// FIXME: What should the actual test case name be here?
|
||||
t.Run("Should not return NoDataFound if all conditions have data and using OR", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeFalse)
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.False(t, context.NoDataFound)
|
||||
})
|
||||
|
||||
Convey("Should return NoDataFound if one condition has no data", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "and", noData: true},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Should return NoDataFound if one condition has no data", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "and", noData: true},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.Equal(t, false, context.Firing)
|
||||
require.True(t, context.NoDataFound)
|
||||
})
|
||||
|
||||
Convey("Should not return no data if at least one condition has no data and using AND", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "and", noData: true},
|
||||
&conditionStub{operator: "and", noData: false},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Should not return no data if at least one condition has no data and using AND", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "and", noData: true},
|
||||
&conditionStub{operator: "and", noData: false},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeFalse)
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.False(t, context.NoDataFound)
|
||||
})
|
||||
|
||||
Convey("Should return no data if at least one condition has no data and using OR", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "or", noData: true},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
t.Run("Should return no data if at least one condition has no data and using OR", func(t *testing.T) {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "or", noData: true},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
},
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
handler.Eval(context)
|
||||
require.True(t, context.NoDataFound)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ func init() {
|
||||
PropertyName: "url",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Label: "Use Discord's Webhook Username",
|
||||
Description: "Use the username configured in Discord's webhook settings. Otherwise, the username will be 'Grafana'",
|
||||
Element: alerting.ElementTypeCheckbox,
|
||||
PropertyName: "use_discord_username",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -58,13 +64,15 @@ func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecrypted
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"}
|
||||
}
|
||||
useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false)
|
||||
|
||||
return &DiscordNotifier{
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Content: content,
|
||||
AvatarURL: avatar,
|
||||
WebhookURL: url,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Content: content,
|
||||
AvatarURL: avatar,
|
||||
WebhookURL: url,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
UseDiscordUsername: useDiscordUsername,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -72,10 +80,11 @@ func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecrypted
|
||||
// notifications to discord.
|
||||
type DiscordNotifier struct {
|
||||
NotifierBase
|
||||
Content string
|
||||
AvatarURL string
|
||||
WebhookURL string
|
||||
log log.Logger
|
||||
Content string
|
||||
AvatarURL string
|
||||
WebhookURL string
|
||||
log log.Logger
|
||||
UseDiscordUsername bool
|
||||
}
|
||||
|
||||
// Notify send an alert notification to Discord.
|
||||
@@ -89,7 +98,9 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("username", "Grafana")
|
||||
if !dn.UseDiscordUsername {
|
||||
bodyJSON.Set("username", "Grafana")
|
||||
}
|
||||
|
||||
if dn.Content != "" {
|
||||
bodyJSON.Set("content", dn.Content)
|
||||
|
||||
@@ -254,7 +254,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
|
||||
if len(extra)+len(message) <= sizeLimit {
|
||||
return message + extra
|
||||
}
|
||||
log.Debugf("Line too long for image caption. value: %s", extra)
|
||||
log.Debug("Line too long for image caption.", "value", extra)
|
||||
return message
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqCont
|
||||
|
||||
org, err := h.SQLStore.GetOrgByName(h.Cfg.AnonymousOrgName)
|
||||
if err != nil {
|
||||
log.Errorf(3, "Anonymous access organization error: '%s': %s", h.Cfg.AnonymousOrgName, err)
|
||||
log.Error("Anonymous access organization error.", "org_name", h.Cfg.AnonymousOrgName, "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -56,13 +56,13 @@ func (ls *Implementation) UpsertUser(cmd *models.UpsertUserCommand) error {
|
||||
return err
|
||||
}
|
||||
if !cmd.SignupAllowed {
|
||||
log.Warnf("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
|
||||
log.Warn("Not allowing login, user not found in internal user database and allow signup = false", "authmode", extUser.AuthModule)
|
||||
return login.ErrInvalidCredentials
|
||||
}
|
||||
|
||||
limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user")
|
||||
if err != nil {
|
||||
log.Warnf("Error getting user quota. error: %v", err)
|
||||
log.Warn("Error getting user quota.", "error", err)
|
||||
return login.ErrGettingUserQuota
|
||||
}
|
||||
if limitReached {
|
||||
|
||||
@@ -694,6 +694,12 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
||||
InputType: alerting.InputTypeText,
|
||||
PropertyName: "avatar_url",
|
||||
},
|
||||
{
|
||||
Label: "Use Discord's Webhook Username",
|
||||
Description: "Use the username configured in Discord's webhook settings. Otherwise, the username will be 'Grafana'",
|
||||
Element: alerting.ElementTypeCheckbox,
|
||||
PropertyName: "use_discord_username",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,11 +18,12 @@ import (
|
||||
|
||||
type DiscordNotifier struct {
|
||||
*Base
|
||||
log log.Logger
|
||||
tmpl *template.Template
|
||||
Content string
|
||||
AvatarURL string
|
||||
WebhookURL string
|
||||
log log.Logger
|
||||
tmpl *template.Template
|
||||
Content string
|
||||
AvatarURL string
|
||||
WebhookURL string
|
||||
UseDiscordUsername bool
|
||||
}
|
||||
|
||||
func NewDiscordNotifier(model *NotificationChannelConfig, t *template.Template) (*DiscordNotifier, error) {
|
||||
@@ -37,6 +38,8 @@ func NewDiscordNotifier(model *NotificationChannelConfig, t *template.Template)
|
||||
return nil, receiverInitError{Reason: "could not find webhook url property in settings", Cfg: *model}
|
||||
}
|
||||
|
||||
useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false)
|
||||
|
||||
content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`)
|
||||
|
||||
return &DiscordNotifier{
|
||||
@@ -48,11 +51,12 @@ func NewDiscordNotifier(model *NotificationChannelConfig, t *template.Template)
|
||||
Settings: model.Settings,
|
||||
SecureSettings: model.SecureSettings,
|
||||
}),
|
||||
Content: content,
|
||||
AvatarURL: avatarURL,
|
||||
WebhookURL: discordURL,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
tmpl: t,
|
||||
Content: content,
|
||||
AvatarURL: avatarURL,
|
||||
WebhookURL: discordURL,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
tmpl: t,
|
||||
UseDiscordUsername: useDiscordUsername,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -60,7 +64,10 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
alerts := types.Alerts(as...)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("username", "Grafana")
|
||||
|
||||
if !d.UseDiscordUsername {
|
||||
bodyJSON.Set("username", "Grafana")
|
||||
}
|
||||
|
||||
var tmplErr error
|
||||
tmpl, _ := TmplText(ctx, d.tmpl, as, d.log, &tmplErr)
|
||||
|
||||
@@ -100,6 +100,35 @@ func TestDiscordNotifier(t *testing.T) {
|
||||
settings: `{}`,
|
||||
expInitError: `failed to validate receiver "discord_testing" of type "discord": could not find webhook url property in settings`,
|
||||
},
|
||||
{
|
||||
name: "Default config with one alert, use default discord username",
|
||||
settings: `{
|
||||
"url": "http://localhost",
|
||||
"use_discord_username": true
|
||||
}`,
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: map[string]interface{}{
|
||||
"content": "**Firing**\n\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",
|
||||
"embeds": []interface{}{map[string]interface{}{
|
||||
"color": 1.4037554e+07,
|
||||
"footer": map[string]interface{}{
|
||||
"icon_url": "https://grafana.com/assets/img/fav32.png",
|
||||
"text": "Grafana v",
|
||||
},
|
||||
"title": "[FIRING:1] (val1)",
|
||||
"url": "http://localhost/alerting/list",
|
||||
"type": "rich",
|
||||
}},
|
||||
},
|
||||
expMsgError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -10,21 +10,23 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/sender"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSendingToExternalAlertmanager(t *testing.T) {
|
||||
@@ -35,7 +37,7 @@ func TestSendingToExternalAlertmanager(t *testing.T) {
|
||||
fakeAdminConfigStore := newFakeAdminConfigStore(t)
|
||||
|
||||
// create alert rule with one second interval
|
||||
alertRule := CreateTestAlertRule(t, fakeRuleStore, 1, 1)
|
||||
alertRule := CreateTestAlertRule(t, fakeRuleStore, 1, 1, eval.Alerting)
|
||||
|
||||
// First, let's create an admin configuration that holds an alertmanager.
|
||||
adminConfig := &models.AdminConfiguration{OrgID: 1, Alertmanagers: []string{fakeAM.server.URL}}
|
||||
@@ -233,6 +235,325 @@ func TestSendingToExternalAlertmanager_WithMultipleOrgs(t *testing.T) {
|
||||
}, 10*time.Second, 200*time.Millisecond, "Alertmanager for org 1 and 2 were never removed")
|
||||
}
|
||||
|
||||
func TestSchedule_ruleRoutine(t *testing.T) {
|
||||
createSchedule := func(
|
||||
evalAppliedChan chan time.Time,
|
||||
) (*schedule, *fakeRuleStore, *fakeInstanceStore, *fakeAdminConfigStore) {
|
||||
ruleStore := newFakeRuleStore(t)
|
||||
instanceStore := &fakeInstanceStore{}
|
||||
adminConfigStore := newFakeAdminConfigStore(t)
|
||||
|
||||
sch, _ := setupScheduler(t, ruleStore, instanceStore, adminConfigStore)
|
||||
|
||||
sch.evalAppliedFunc = func(key models.AlertRuleKey, t time.Time) {
|
||||
evalAppliedChan <- t
|
||||
}
|
||||
return sch, ruleStore, instanceStore, adminConfigStore
|
||||
}
|
||||
|
||||
// normal states do not include NoData and Error because currently it is not possible to perform any sensible test
|
||||
normalStates := []eval.State{eval.Normal, eval.Alerting, eval.Pending}
|
||||
randomNormalState := func() eval.State {
|
||||
// pick only supported cases
|
||||
return normalStates[rand.Intn(3)]
|
||||
}
|
||||
|
||||
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)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
sch, ruleStore, instanceStore, _ := createSchedule(evalAppliedChan)
|
||||
|
||||
rule := CreateTestAlertRule(t, ruleStore, 10, rand.Int63(), evalState)
|
||||
|
||||
go func() {
|
||||
stop := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(stop)
|
||||
})
|
||||
_ = sch.ruleRoutine(context.Background(), rule.GetKey(), evalChan, stop)
|
||||
}()
|
||||
|
||||
expectedTime := time.UnixMicro(rand.Int63())
|
||||
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
|
||||
actualTime := waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
|
||||
t.Run("it should get rule from database when run the first time", func(t *testing.T) {
|
||||
queries := make([]models.GetAlertRuleByUIDQuery, 0)
|
||||
for _, op := range ruleStore.recordedOps {
|
||||
switch q := op.(type) {
|
||||
case models.GetAlertRuleByUIDQuery:
|
||||
queries = append(queries, q)
|
||||
}
|
||||
}
|
||||
require.NotEmptyf(t, queries, "Expected a %T request to rule store but nothing was recorded", models.GetAlertRuleByUIDQuery{})
|
||||
require.Len(t, queries, 1, "Expected exactly one request of %T but got %d", models.GetAlertRuleByUIDQuery{}, len(queries))
|
||||
require.Equal(t, rule.UID, queries[0].UID)
|
||||
require.Equal(t, rule.OrgID, queries[0].OrgID)
|
||||
})
|
||||
t.Run("it should process evaluation results via state manager", func(t *testing.T) {
|
||||
// TODO rewrite when we are able to mock/fake state manager
|
||||
states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||
require.Len(t, states, 1)
|
||||
s := states[0]
|
||||
t.Logf("State: %v", s)
|
||||
require.Equal(t, rule.UID, s.AlertRuleUID)
|
||||
require.Len(t, s.Results, 1)
|
||||
var expectedStatus = evalState
|
||||
if evalState == eval.Pending {
|
||||
expectedStatus = eval.Alerting
|
||||
}
|
||||
require.Equal(t, expectedStatus.String(), s.Results[0].EvaluationState.String())
|
||||
require.Equal(t, expectedTime, s.Results[0].EvaluationTime)
|
||||
})
|
||||
t.Run("it should save alert instances to storage", func(t *testing.T) {
|
||||
// TODO rewrite when we are able to mock/fake state manager
|
||||
states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||
require.Len(t, states, 1)
|
||||
s := states[0]
|
||||
|
||||
var cmd *models.SaveAlertInstanceCommand
|
||||
for _, op := range instanceStore.recordedOps {
|
||||
switch q := op.(type) {
|
||||
case models.SaveAlertInstanceCommand:
|
||||
cmd = &q
|
||||
}
|
||||
if cmd != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, cmd)
|
||||
t.Logf("Saved alert instance: %v", cmd)
|
||||
require.Equal(t, rule.OrgID, cmd.RuleOrgID)
|
||||
require.Equal(t, expectedTime, cmd.LastEvalTime)
|
||||
require.Equal(t, cmd.RuleUID, cmd.RuleUID)
|
||||
require.Equal(t, evalState.String(), string(cmd.State))
|
||||
require.Equal(t, s.Labels, data.Labels(cmd.Labels))
|
||||
})
|
||||
t.Run("it reports metrics", func(t *testing.T) {
|
||||
// TODO fix it when we update the way we use metrics
|
||||
t.Skip()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("should exit", func(t *testing.T) {
|
||||
t.Run("when we signal it to stop", func(t *testing.T) {
|
||||
stopChan := make(chan struct{})
|
||||
stoppedChan := make(chan error)
|
||||
|
||||
sch, _, _, _ := createSchedule(make(chan time.Time))
|
||||
|
||||
go func() {
|
||||
err := sch.ruleRoutine(context.Background(), models.AlertRuleKey{}, make(chan *evalContext), stopChan)
|
||||
stoppedChan <- err
|
||||
}()
|
||||
|
||||
stopChan <- struct{}{}
|
||||
err := waitForErrChannel(t, stoppedChan)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("when context is cancelled", func(t *testing.T) {
|
||||
stoppedChan := make(chan error)
|
||||
sch, _, _, _ := createSchedule(make(chan time.Time))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
err := sch.ruleRoutine(ctx, models.AlertRuleKey{}, make(chan *evalContext), make(chan struct{}))
|
||||
stoppedChan <- err
|
||||
}()
|
||||
|
||||
cancel()
|
||||
err := waitForErrChannel(t, stoppedChan)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("should fetch rule from database only if new version is greater than current", func(t *testing.T) {
|
||||
evalChan := make(chan *evalContext)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
sch, ruleStore, _, _ := createSchedule(evalAppliedChan)
|
||||
|
||||
rule := CreateTestAlertRule(t, ruleStore, 10, rand.Int63(), randomNormalState())
|
||||
|
||||
go func() {
|
||||
stop := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(stop)
|
||||
})
|
||||
_ = sch.ruleRoutine(context.Background(), rule.GetKey(), evalChan, stop)
|
||||
}()
|
||||
|
||||
expectedTime := time.UnixMicro(rand.Int63())
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
|
||||
actualTime := waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
|
||||
// Now update the rule
|
||||
newRule := *rule
|
||||
newRule.Version++
|
||||
ruleStore.putRule(&newRule)
|
||||
|
||||
// and call with new version
|
||||
expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second)
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: newRule.Version,
|
||||
}
|
||||
|
||||
actualTime = waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
|
||||
queries := make([]models.GetAlertRuleByUIDQuery, 0)
|
||||
for _, op := range ruleStore.recordedOps {
|
||||
switch q := op.(type) {
|
||||
case models.GetAlertRuleByUIDQuery:
|
||||
queries = append(queries, q)
|
||||
}
|
||||
}
|
||||
require.Len(t, queries, 2, "Expected exactly two request of %T", models.GetAlertRuleByUIDQuery{})
|
||||
require.Equal(t, rule.UID, queries[0].UID)
|
||||
require.Equal(t, rule.OrgID, queries[0].OrgID)
|
||||
require.Equal(t, rule.UID, queries[1].UID)
|
||||
require.Equal(t, rule.OrgID, queries[1].OrgID)
|
||||
})
|
||||
|
||||
t.Run("should not fetch rule if version is equal or less than current", func(t *testing.T) {
|
||||
evalChan := make(chan *evalContext)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
sch, ruleStore, _, _ := createSchedule(evalAppliedChan)
|
||||
|
||||
rule := CreateTestAlertRule(t, ruleStore, 10, rand.Int63(), randomNormalState())
|
||||
|
||||
go func() {
|
||||
stop := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(stop)
|
||||
})
|
||||
_ = sch.ruleRoutine(context.Background(), rule.GetKey(), evalChan, stop)
|
||||
}()
|
||||
|
||||
expectedTime := time.UnixMicro(rand.Int63())
|
||||
evalChan <- &evalContext{
|
||||
now: expectedTime,
|
||||
version: rule.Version,
|
||||
}
|
||||
|
||||
actualTime := waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
|
||||
// try again with the same version
|
||||
expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second)
|
||||
evalChan <- &evalContext{
|
||||
now: 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,
|
||||
}
|
||||
actualTime = waitForTimeChannel(t, evalAppliedChan)
|
||||
require.Equal(t, expectedTime, actualTime)
|
||||
|
||||
queries := make([]models.GetAlertRuleByUIDQuery, 0)
|
||||
for _, op := range ruleStore.recordedOps {
|
||||
switch q := op.(type) {
|
||||
case models.GetAlertRuleByUIDQuery:
|
||||
queries = append(queries, q)
|
||||
}
|
||||
}
|
||||
require.Len(t, queries, 1, "Expected exactly one request of %T", models.GetAlertRuleByUIDQuery{})
|
||||
})
|
||||
|
||||
t.Run("when evaluation fails", func(t *testing.T) {
|
||||
t.Run("it should increase failure counter", func(t *testing.T) {
|
||||
t.Skip()
|
||||
// TODO implement check for counter
|
||||
})
|
||||
t.Run("it should retry up to configured times", func(t *testing.T) {
|
||||
// TODO figure out how to simulate failure
|
||||
t.Skip()
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when there are alerts that should be firing", func(t *testing.T) {
|
||||
t.Run("it should send to local alertmanager if configured for organization", func(t *testing.T) {
|
||||
// TODO figure out how to simulate multiorg alertmanager
|
||||
t.Skip()
|
||||
})
|
||||
t.Run("it should send to external alertmanager if configured for organization", func(t *testing.T) {
|
||||
fakeAM := NewFakeExternalAlertmanager(t)
|
||||
defer fakeAM.Close()
|
||||
|
||||
orgID := rand.Int63()
|
||||
s, err := sender.New(nil)
|
||||
require.NoError(t, err)
|
||||
adminConfig := &models.AdminConfiguration{OrgID: orgID, Alertmanagers: []string{fakeAM.server.URL}}
|
||||
err = s.ApplyConfig(adminConfig)
|
||||
require.NoError(t, err)
|
||||
s.Run()
|
||||
defer s.Stop()
|
||||
|
||||
require.Eventuallyf(t, func() bool {
|
||||
return len(s.Alertmanagers()) == 1
|
||||
}, 20*time.Second, 200*time.Millisecond, "external Alertmanager was not discovered.")
|
||||
|
||||
evalChan := make(chan *evalContext)
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
sch, ruleStore, _, _ := createSchedule(evalAppliedChan)
|
||||
sch.senders[orgID] = s
|
||||
// eval.Alerting makes state manager to create notifications for alertmanagers
|
||||
rule := CreateTestAlertRule(t, ruleStore, 10, orgID, eval.Alerting)
|
||||
|
||||
go func() {
|
||||
stop := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(stop)
|
||||
})
|
||||
_ = sch.ruleRoutine(context.Background(), rule.GetKey(), evalChan, stop)
|
||||
}()
|
||||
|
||||
evalChan <- &evalContext{
|
||||
now: time.Now(),
|
||||
version: rule.Version,
|
||||
}
|
||||
waitForTimeChannel(t, evalAppliedChan)
|
||||
|
||||
var count int
|
||||
require.Eventuallyf(t, func() bool {
|
||||
count = fakeAM.AlertsCount()
|
||||
return count == 1 && fakeAM.AlertNamesCompare([]string{rule.Title})
|
||||
}, 20*time.Second, 200*time.Millisecond, "Alertmanager never received an '%s', received alerts count: %d", rule.Title, count)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when there are no alerts to send it should not call notifiers", func(t *testing.T) {
|
||||
// TODO needs some mocking/stubbing for Alertmanager and Sender to make sure it was not called
|
||||
t.Skip()
|
||||
})
|
||||
}
|
||||
|
||||
func setupScheduler(t *testing.T, rs store.RuleStore, is store.InstanceStore, acs store.AdminConfigurationStore) (*schedule, *clock.Mock) {
|
||||
t.Helper()
|
||||
|
||||
@@ -266,11 +587,46 @@ func setupScheduler(t *testing.T, rs store.RuleStore, is store.InstanceStore, ac
|
||||
}
|
||||
|
||||
// createTestAlertRule creates a dummy alert definition to be used by the tests.
|
||||
func CreateTestAlertRule(t *testing.T, dbstore *fakeRuleStore, intervalSeconds int64, orgID int64) *models.AlertRule {
|
||||
func CreateTestAlertRule(t *testing.T, dbstore *fakeRuleStore, intervalSeconds int64, orgID int64, evalResult eval.State) *models.AlertRule {
|
||||
t.Helper()
|
||||
|
||||
records := make([]interface{}, 0, len(dbstore.recordedOps))
|
||||
copy(records, dbstore.recordedOps)
|
||||
defer func() {
|
||||
// erase queries that were made by the testing suite
|
||||
dbstore.recordedOps = records
|
||||
}()
|
||||
d := rand.Intn(1000)
|
||||
ruleGroup := fmt.Sprintf("ruleGroup-%d", d)
|
||||
|
||||
var expression string
|
||||
var forDuration time.Duration
|
||||
switch evalResult {
|
||||
case eval.Normal:
|
||||
expression = `{
|
||||
"datasourceUid": "-100",
|
||||
"type":"math",
|
||||
"expression":"2 + 1 < 1"
|
||||
}`
|
||||
case eval.Pending, eval.Alerting:
|
||||
expression = `{
|
||||
"datasourceUid": "-100",
|
||||
"type":"math",
|
||||
"expression":"2 + 2 > 1"
|
||||
}`
|
||||
if evalResult == eval.Pending {
|
||||
forDuration = 100 * time.Second
|
||||
}
|
||||
case eval.Error:
|
||||
expression = `{
|
||||
"datasourceUid": "-100",
|
||||
"type":"math",
|
||||
"expression":"$A"
|
||||
}`
|
||||
case eval.NoData:
|
||||
// TODO Implement support for NoData
|
||||
require.Fail(t, "Alert rule with desired evaluation result NoData is not supported yet")
|
||||
}
|
||||
|
||||
err := dbstore.UpdateRuleGroup(store.UpdateRuleGroupCmd{
|
||||
OrgID: orgID,
|
||||
NamespaceUID: "namespace",
|
||||
@@ -281,6 +637,7 @@ func CreateTestAlertRule(t *testing.T, dbstore *fakeRuleStore, intervalSeconds i
|
||||
{
|
||||
ApiRuleNode: &apimodels.ApiRuleNode{
|
||||
Annotations: map[string]string{"testAnnoKey": "testAnnoValue"},
|
||||
For: model.Duration(forDuration),
|
||||
},
|
||||
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||
Title: fmt.Sprintf("an alert definition %d", d),
|
||||
@@ -288,11 +645,7 @@ func CreateTestAlertRule(t *testing.T, dbstore *fakeRuleStore, intervalSeconds i
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
DatasourceUID: "-100",
|
||||
Model: json.RawMessage(`{
|
||||
"datasourceUid": "-100",
|
||||
"type":"math",
|
||||
"expression":"2 + 2 > 1"
|
||||
}`),
|
||||
Model: json.RawMessage(expression),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(5 * time.Hour),
|
||||
To: models.Duration(3 * time.Hour),
|
||||
|
||||
@@ -21,15 +21,52 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// waitForTimeChannel blocks the execution until either the channel ch has some data or a timeout of 10 second expires.
|
||||
// Timeout will cause the test to fail.
|
||||
// Returns the data from the channel.
|
||||
func waitForTimeChannel(t *testing.T, ch chan time.Time) time.Time {
|
||||
select {
|
||||
case result := <-ch:
|
||||
return result
|
||||
case <-time.After(time.Duration(10) * time.Second):
|
||||
t.Fatalf("Timeout waiting for data in the time channel")
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// waitForErrChannel blocks the execution until either the channel ch has some data or a timeout of 10 second expires.
|
||||
// Timeout will cause the test to fail.
|
||||
// Returns the data from the channel.
|
||||
func waitForErrChannel(t *testing.T, ch chan error) error {
|
||||
timeout := time.Duration(10) * time.Second
|
||||
select {
|
||||
case result := <-ch:
|
||||
return result
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("Timeout waiting for data in the error channel")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeRuleStore(t *testing.T) *fakeRuleStore {
|
||||
return &fakeRuleStore{t: t, rules: map[int64]map[string]map[string][]*models.AlertRule{}}
|
||||
}
|
||||
|
||||
// FakeRuleStore mocks the RuleStore of the scheduler.
|
||||
type fakeRuleStore struct {
|
||||
t *testing.T
|
||||
mtx sync.Mutex
|
||||
rules map[int64]map[string]map[string][]*models.AlertRule
|
||||
t *testing.T
|
||||
mtx sync.Mutex
|
||||
rules map[int64]map[string]map[string][]*models.AlertRule
|
||||
recordedOps []interface{}
|
||||
}
|
||||
|
||||
// putRule puts the rule in the rules map. If there are existing rule in the same namespace, they will be overwritten
|
||||
func (f *fakeRuleStore) putRule(r *models.AlertRule) {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.rules[r.OrgID][r.RuleGroup][r.NamespaceUID] = []*models.AlertRule{
|
||||
r,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeRuleStore) DeleteAlertRuleByUID(_ int64, _ string) error { return nil }
|
||||
@@ -43,7 +80,7 @@ func (f *fakeRuleStore) DeleteAlertInstancesByRuleUID(_ int64, _ string) error {
|
||||
func (f *fakeRuleStore) GetAlertRuleByUID(q *models.GetAlertRuleByUIDQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
rgs, ok := f.rules[q.OrgID]
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -67,7 +104,7 @@ func (f *fakeRuleStore) GetAlertRuleByUID(q *models.GetAlertRuleByUIDQuery) erro
|
||||
func (f *fakeRuleStore) GetAlertRulesForScheduling(q *models.ListAlertRulesQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
for _, rg := range f.rules {
|
||||
for _, n := range rg {
|
||||
for _, r := range n {
|
||||
@@ -78,13 +115,22 @@ func (f *fakeRuleStore) GetAlertRulesForScheduling(q *models.ListAlertRulesQuery
|
||||
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRuleStore) GetOrgAlertRules(_ *models.ListAlertRulesQuery) error { return nil }
|
||||
func (f *fakeRuleStore) GetNamespaceAlertRules(_ *models.ListNamespaceAlertRulesQuery) error {
|
||||
func (f *fakeRuleStore) GetOrgAlertRules(q *models.ListAlertRulesQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRuleStore) GetNamespaceAlertRules(q *models.ListNamespaceAlertRulesQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRuleStore) GetRuleGroupAlertRules(q *models.ListRuleGroupAlertRulesQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
rgs, ok := f.rules[q.OrgID]
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -116,11 +162,23 @@ func (f *fakeRuleStore) GetNamespaces(_ context.Context, _ int64, _ *models2.Sig
|
||||
func (f *fakeRuleStore) GetNamespaceByTitle(_ context.Context, _ string, _ int64, _ *models2.SignedInUser, _ bool) (*models2.Folder, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeRuleStore) GetOrgRuleGroups(_ *models.ListOrgRuleGroupsQuery) error { return nil }
|
||||
func (f *fakeRuleStore) UpsertAlertRules(_ []store.UpsertRule) error { return nil }
|
||||
func (f *fakeRuleStore) GetOrgRuleGroups(q *models.ListOrgRuleGroupsQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleStore) UpsertAlertRules(q []store.UpsertRule) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, q)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRuleStore) UpdateRuleGroup(cmd store.UpdateRuleGroupCmd) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, cmd)
|
||||
rgs, ok := f.rules[cmd.OrgID]
|
||||
if !ok {
|
||||
f.rules[cmd.OrgID] = map[string]map[string][]*models.AlertRule{}
|
||||
@@ -138,7 +196,7 @@ func (f *fakeRuleStore) UpdateRuleGroup(cmd store.UpdateRuleGroupCmd) error {
|
||||
|
||||
rules := []*models.AlertRule{}
|
||||
for _, r := range cmd.RuleGroupConfig.Rules {
|
||||
//TODO: Not sure why this is not being set properly, where is the code that sets this?
|
||||
// TODO: Not sure why this is not being set properly, where is the code that sets this?
|
||||
for i := range r.GrafanaManagedAlert.Data {
|
||||
r.GrafanaManagedAlert.Data[i].DatasourceUID = "-100"
|
||||
}
|
||||
@@ -181,13 +239,32 @@ func (f *fakeRuleStore) UpdateRuleGroup(cmd store.UpdateRuleGroupCmd) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeInstanceStore struct{}
|
||||
type fakeInstanceStore struct {
|
||||
mtx sync.Mutex
|
||||
recordedOps []interface{}
|
||||
}
|
||||
|
||||
func (f *fakeInstanceStore) GetAlertInstance(_ *models.GetAlertInstanceQuery) error { return nil }
|
||||
func (f *fakeInstanceStore) ListAlertInstances(_ *models.ListAlertInstancesQuery) error { return nil }
|
||||
func (f *fakeInstanceStore) SaveAlertInstance(_ *models.SaveAlertInstanceCommand) error { return nil }
|
||||
func (f *fakeInstanceStore) FetchOrgIds() ([]int64, error) { return []int64{}, nil }
|
||||
func (f *fakeInstanceStore) DeleteAlertInstance(_ int64, _, _ string) error { return nil }
|
||||
func (f *fakeInstanceStore) GetAlertInstance(q *models.GetAlertInstanceQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeInstanceStore) ListAlertInstances(q *models.ListAlertInstancesQuery) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeInstanceStore) SaveAlertInstance(q *models.SaveAlertInstanceCommand) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
f.recordedOps = append(f.recordedOps, *q)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInstanceStore) FetchOrgIds() ([]int64, error) { return []int64{}, nil }
|
||||
func (f *fakeInstanceStore) DeleteAlertInstance(_ int64, _, _ string) error { return nil }
|
||||
|
||||
func newFakeAdminConfigStore(t *testing.T) *fakeAdminConfigStore {
|
||||
t.Helper()
|
||||
|
||||
@@ -5,34 +5,35 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmailCodes(t *testing.T) {
|
||||
Convey("When generating code", t, func() {
|
||||
t.Run("When generating code", func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.EmailCodeValidMinutes = 120
|
||||
|
||||
user := &models.User{Id: 10, Email: "t@a.com", Login: "asd", Password: "1", Rands: "2"}
|
||||
code, err := createUserEmailCode(cfg, user, nil)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
Convey("getLoginForCode should return login", func() {
|
||||
t.Run("getLoginForCode should return login", func(t *testing.T) {
|
||||
login := getLoginForEmailCode(code)
|
||||
So(login, ShouldEqual, "asd")
|
||||
require.Equal(t, login, "asd")
|
||||
})
|
||||
|
||||
Convey("Can verify valid code", func() {
|
||||
t.Run("Can verify valid code", func(t *testing.T) {
|
||||
isValid, err := validateUserEmailCode(cfg, user, code)
|
||||
So(err, ShouldBeNil)
|
||||
So(isValid, ShouldBeTrue)
|
||||
require.NoError(t, err)
|
||||
require.True(t, isValid)
|
||||
})
|
||||
|
||||
Convey("Cannot verify in-valid code", func() {
|
||||
t.Run("Cannot verify in-valid code", func(t *testing.T) {
|
||||
code = "ASD"
|
||||
isValid, err := validateUserEmailCode(cfg, user, code)
|
||||
So(err, ShouldBeNil)
|
||||
So(isValid, ShouldBeFalse)
|
||||
require.NoError(t, err)
|
||||
require.False(t, isValid)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmailIntegrationTest(t *testing.T) {
|
||||
SkipConvey("Given the notifications service", t, func() {
|
||||
t.Run("Given the notifications service", func(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
setting.StaticRootPath = "../../../public/"
|
||||
setting.BuildVersion = "4.0.0"
|
||||
|
||||
@@ -24,7 +27,7 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
ns.Cfg.Smtp.FromName = "Grafana Admin"
|
||||
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
|
||||
|
||||
Convey("When sending reset email password", func() {
|
||||
t.Run("When sending reset email password", func(t *testing.T) {
|
||||
cmd := &models.SendEmailCommand{
|
||||
|
||||
Data: map[string]interface{}{
|
||||
@@ -54,15 +57,15 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
}
|
||||
|
||||
err := ns.sendEmailCommandHandler(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
sentMsg := <-ns.mailQueue
|
||||
So(sentMsg.From, ShouldEqual, "Grafana Admin <from@address.com>")
|
||||
So(sentMsg.To[0], ShouldEqual, "asdf@asdf.com")
|
||||
require.Equal(t, sentMsg.From, "Grafana Admin <from@address.com>")
|
||||
require.Equal(t, sentMsg.To[0], "asdf@asdf.com")
|
||||
err = ioutil.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body["text/html"]), 0777)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
err = ioutil.WriteFile("../../../tmp/test_email.txt", []byte(sentMsg.Body["text/plain"]), 0777)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (o *OSSSearchUserFilter) GetFilter(filterName string, params []string) mode
|
||||
}
|
||||
filter, err := f(params)
|
||||
if err != nil {
|
||||
log.Warnf("Cannot initialise the filter %s: %s", filterName, err)
|
||||
log.Warn("Cannot initialise the filter.", "filter", filterName, "error", err)
|
||||
return nil
|
||||
}
|
||||
return filter
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ualert
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -16,7 +18,8 @@ func (s SecureJsonData) DecryptedValue(key string) (string, bool) {
|
||||
if value, ok := s[key]; ok {
|
||||
decryptedData, err := util.Decrypt(value, setting.SecretKey)
|
||||
if err != nil {
|
||||
log.Fatalf(4, err.Error())
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
return string(decryptedData), true
|
||||
}
|
||||
@@ -30,7 +33,8 @@ func (s SecureJsonData) Decrypt() map[string]string {
|
||||
for key, data := range s {
|
||||
decryptedData, err := util.Decrypt(data, setting.SecretKey)
|
||||
if err != nil {
|
||||
log.Fatalf(4, err.Error())
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
decrypted[key] = string(decryptedData)
|
||||
@@ -44,7 +48,8 @@ func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
|
||||
for key, data := range sjd {
|
||||
encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey)
|
||||
if err != nil {
|
||||
log.Fatalf(4, err.Error())
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
encrypted[key] = encryptedData
|
||||
|
||||
@@ -50,7 +50,7 @@ func (e *MigrationError) Unwrap() error { return e.Err }
|
||||
func AddDashAlertMigration(mg *migrator.Migrator) {
|
||||
logs, err := mg.GetMigrationLog()
|
||||
if err != nil {
|
||||
mg.Logger.Crit("alert migration failure: could not get migration log", "error", err)
|
||||
mg.Logger.Error("alert migration failure: could not get migration log", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func AddDashAlertMigration(mg *migrator.Migrator) {
|
||||
func RerunDashAlertMigration(mg *migrator.Migrator) {
|
||||
logs, err := mg.GetMigrationLog()
|
||||
if err != nil {
|
||||
mg.Logger.Crit("alert migration failure: could not get migration log", "error", err)
|
||||
mg.Logger.Error("alert migration failure: could not get migration log", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func RerunDashAlertMigration(mg *migrator.Migrator) {
|
||||
func AddDashboardUIDPanelIDMigration(mg *migrator.Migrator) {
|
||||
logs, err := mg.GetMigrationLog()
|
||||
if err != nil {
|
||||
mg.Logger.Crit("alert migration failure: could not get migration log", "error", err)
|
||||
mg.Logger.Error("alert migration failure: could not get migration log", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,11 @@ func (ss *SQLStore) Reset() error {
|
||||
return ss.ensureMainOrgAndAdminUser()
|
||||
}
|
||||
|
||||
// Quote quotes the value in the used SQL dialect
|
||||
func (ss *SQLStore) Quote(value string) string {
|
||||
return ss.engine.Quote(value)
|
||||
}
|
||||
|
||||
func (ss *SQLStore) ensureMainOrgAndAdminUser() error {
|
||||
ctx := context.Background()
|
||||
err := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
|
||||
|
||||
@@ -67,7 +67,7 @@ func inTransactionWithRetryCtx(ctx context.Context, engine *xorm.Engine, callbac
|
||||
if len(sess.events) > 0 {
|
||||
for _, e := range sess.events {
|
||||
if err = bus.Publish(e); err != nil {
|
||||
log.Errorf(3, "Failed to publish event after commit. error: %v", err)
|
||||
log.Error("Failed to publish event after commit.", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateTeam(t *testing.T) {
|
||||
Convey("Updating a team", t, func() {
|
||||
t.Run("Updating a team", func(t *testing.T) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
admin := models.SignedInUser{
|
||||
@@ -27,20 +28,20 @@ func TestUpdateTeam(t *testing.T) {
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
Convey("Given an editor and a team he isn't a member of", func() {
|
||||
Convey("Should not be able to update the team", func() {
|
||||
t.Run("Given an editor and a team he isn't a member of", func(t *testing.T) {
|
||||
t.Run("Should not be able to update the team", func(t *testing.T) {
|
||||
bus.AddHandler("test", func(cmd *models.GetTeamMembersQuery) error {
|
||||
cmd.Result = []*models.TeamMemberDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor)
|
||||
So(err, ShouldEqual, models.ErrNotAllowedToUpdateTeam)
|
||||
require.Equal(t, models.ErrNotAllowedToUpdateTeam, err)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an editor and a team he is an admin in", func() {
|
||||
Convey("Should be able to update the team", func() {
|
||||
t.Run("Given an editor and a team he is an admin in", func(t *testing.T) {
|
||||
t.Run("Should be able to update the team", func(t *testing.T) {
|
||||
bus.AddHandler("test", func(cmd *models.GetTeamMembersQuery) error {
|
||||
cmd.Result = []*models.TeamMemberDTO{{
|
||||
OrgId: testTeam.OrgId,
|
||||
@@ -52,17 +53,17 @@ func TestUpdateTeam(t *testing.T) {
|
||||
})
|
||||
|
||||
err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an editor and a team in another org", func() {
|
||||
t.Run("Given an editor and a team in another org", func(t *testing.T) {
|
||||
testTeamOtherOrg := models.Team{
|
||||
Id: 1,
|
||||
OrgId: 2,
|
||||
}
|
||||
|
||||
Convey("Shouldn't be able to update the team", func() {
|
||||
t.Run("Shouldn't be able to update the team", func(t *testing.T) {
|
||||
bus.AddHandler("test", func(cmd *models.GetTeamMembersQuery) error {
|
||||
cmd.Result = []*models.TeamMemberDTO{{
|
||||
OrgId: testTeamOtherOrg.OrgId,
|
||||
@@ -74,14 +75,14 @@ func TestUpdateTeam(t *testing.T) {
|
||||
})
|
||||
|
||||
err := CanAdmin(bus.GetBus(), testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor)
|
||||
So(err, ShouldEqual, models.ErrNotAllowedToUpdateTeamInDifferentOrg)
|
||||
require.Equal(t, models.ErrNotAllowedToUpdateTeamInDifferentOrg, err)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an org admin and a team", func() {
|
||||
Convey("Should be able to update the team", func() {
|
||||
t.Run("Given an org admin and a team", func(t *testing.T) {
|
||||
t.Run("Should be able to update the team", func(t *testing.T) {
|
||||
err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &admin)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -464,7 +464,8 @@ func parseAppUrlAndSubUrl(section *ini.Section) (string, string, error) {
|
||||
// Check if has app suburl.
|
||||
url, err := url.Parse(appUrl)
|
||||
if err != nil {
|
||||
log.Fatalf(4, "Invalid root_url(%s): %s", appUrl, err)
|
||||
log.Error("Invalid root_url.", "url", appUrl, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appSubUrl := strings.TrimSuffix(url.Path, "/")
|
||||
@@ -631,8 +632,8 @@ func getCommandLineProperties(args []string) map[string]string {
|
||||
trimmed := strings.TrimPrefix(arg, "cfg:")
|
||||
parts := strings.Split(trimmed, "=")
|
||||
if len(parts) != 2 {
|
||||
log.Fatalf(3, "Invalid command line argument. argument: %v", arg)
|
||||
return nil
|
||||
log.Error("Invalid command line argument.", "argument", arg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
props[parts[0]] = parts[1]
|
||||
@@ -718,7 +719,8 @@ func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) {
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
log.Fatalf(3, err.Error())
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// apply environment overrides
|
||||
@@ -961,7 +963,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
|
||||
cfg.readDataSourcesSettings()
|
||||
|
||||
if VerifyEmailEnabled && !cfg.Smtp.Enabled {
|
||||
log.Warnf("require_email_validation is enabled but smtp is disabled")
|
||||
log.Warn("require_email_validation is enabled but smtp is disabled")
|
||||
}
|
||||
|
||||
// check old key name
|
||||
@@ -1356,7 +1358,8 @@ func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error {
|
||||
_, err := url.Parse(cfg.RendererCallbackUrl)
|
||||
if err != nil {
|
||||
// XXX: Should return an error?
|
||||
log.Fatalf(4, "Invalid callback_url(%s): %s", cfg.RendererCallbackUrl, err)
|
||||
log.Error("Invalid callback_url.", "url", cfg.RendererCallbackUrl, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testLogger struct {
|
||||
@@ -24,24 +25,22 @@ func (stub *testLogger) Info(testMessage string, ctx ...interface{}) {
|
||||
}
|
||||
|
||||
func TestSessionSettings(t *testing.T) {
|
||||
Convey("session config", t, func() {
|
||||
skipStaticRootValidation = true
|
||||
skipStaticRootValidation = true
|
||||
|
||||
Convey("Reading session should log error ", func() {
|
||||
cfg := NewCfg()
|
||||
homePath := "../../"
|
||||
t.Run("Reading session should log error ", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
homePath := "../../"
|
||||
|
||||
stub := &testLogger{}
|
||||
cfg.Logger = stub
|
||||
stub := &testLogger{}
|
||||
cfg.Logger = stub
|
||||
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: homePath,
|
||||
Config: filepath.Join(homePath, "pkg/setting/testdata/session.ini"),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(stub.warnCalled, ShouldEqual, true)
|
||||
So(len(stub.warnMessage), ShouldBeGreaterThan, 0)
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: homePath,
|
||||
Config: filepath.Join(homePath, "pkg/setting/testdata/session.ini"),
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, true, stub.warnCalled)
|
||||
require.Greater(t, len(stub.warnMessage), 0)
|
||||
})
|
||||
}
|
||||
|
||||
+264
-268
@@ -15,8 +15,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,281 +22,279 @@ const (
|
||||
)
|
||||
|
||||
func TestLoadingSettings(t *testing.T) {
|
||||
Convey("Testing loading settings from ini file", t, func() {
|
||||
skipStaticRootValidation = true
|
||||
skipStaticRootValidation = true
|
||||
|
||||
Convey("Given the default ini files", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{HomePath: "../../", Config: "../../conf/defaults.ini"})
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("Given the default ini files", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{HomePath: "../../", Config: "../../conf/defaults.ini"})
|
||||
require.Nil(t, err)
|
||||
|
||||
So(cfg.AdminUser, ShouldEqual, "admin")
|
||||
So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/")
|
||||
})
|
||||
|
||||
Convey("default.ini should have no semi-colon commented entries", func() {
|
||||
file, err := os.Open("../../conf/defaults.ini")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load defaults.ini file: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
So(err, ShouldBeNil)
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
// This only catches values commented out with ";" and will not catch those that are commented out with "#".
|
||||
if strings.HasPrefix(scanner.Text(), ";") {
|
||||
t.Errorf("entries in defaults.ini must not be commented or environment variables will not work: %v", scanner.Text())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Convey("sample.ini should load successfully", func() {
|
||||
customInitPath := CustomInitPath
|
||||
CustomInitPath = "conf/sample.ini"
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
So(err, ShouldBeNil)
|
||||
// Restore CustomInitPath to avoid side effects.
|
||||
CustomInitPath = customInitPath
|
||||
})
|
||||
|
||||
Convey("Should be able to override via environment variables", func() {
|
||||
err := os.Setenv("GF_SECURITY_ADMIN_USER", "superduper")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.AdminUser, ShouldEqual, "superduper")
|
||||
So(cfg.DataPath, ShouldEqual, filepath.Join(HomePath, "data"))
|
||||
So(cfg.LogsPath, ShouldEqual, filepath.Join(cfg.DataPath, "log"))
|
||||
})
|
||||
|
||||
Convey("Should replace password when defined in environment", func() {
|
||||
err := os.Setenv("GF_SECURITY_ADMIN_PASSWORD", "supersecret")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********")
|
||||
})
|
||||
|
||||
Convey("Should replace password in URL when url environment is defined", func() {
|
||||
err := os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(appliedEnvOverrides, ShouldContain, "GF_DATABASE_URL=mysql://user:xxxxx@localhost:3306/database")
|
||||
})
|
||||
|
||||
Convey("Should get property map from command line args array", func() {
|
||||
props := getCommandLineProperties([]string{"cfg:test=value", "cfg:map.test=1"})
|
||||
|
||||
So(len(props), ShouldEqual, 2)
|
||||
So(props["test"], ShouldEqual, "value")
|
||||
So(props["map.test"], ShouldEqual, "1")
|
||||
})
|
||||
|
||||
Convey("Should be able to override via command line", func() {
|
||||
if runtime.GOOS == windows {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{`cfg:paths.data=c:\tmp\data`, `cfg:paths.logs=c:\tmp\logs`},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
So(cfg.DataPath, ShouldEqual, `c:\tmp\data`)
|
||||
So(cfg.LogsPath, ShouldEqual, `c:\tmp\logs`)
|
||||
} else {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, "/tmp/data")
|
||||
So(cfg.LogsPath, ShouldEqual, "/tmp/logs")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Should be able to override defaults via command line", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{
|
||||
"cfg:default.server.domain=test2",
|
||||
},
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.Domain, ShouldEqual, "test2")
|
||||
})
|
||||
|
||||
Convey("Defaults can be overridden in specified config file", func() {
|
||||
if runtime.GOOS == windows {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"),
|
||||
Args: []string{`cfg:default.paths.data=c:\tmp\data`},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, `c:\tmp\override`)
|
||||
} else {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
|
||||
Args: []string{"cfg:default.paths.data=/tmp/data"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, "/tmp/override")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Command line overrides specified config file", func() {
|
||||
if runtime.GOOS == windows {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"),
|
||||
Args: []string{`cfg:paths.data=c:\tmp\data`},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, `c:\tmp\data`)
|
||||
} else {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
|
||||
Args: []string{"cfg:paths.data=/tmp/data"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, "/tmp/data")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Can use environment variables in config values", func() {
|
||||
if runtime.GOOS == windows {
|
||||
err := os.Setenv("GF_DATA_PATH", `c:\tmp\env_override`)
|
||||
require.NoError(t, err)
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, `c:\tmp\env_override`)
|
||||
} else {
|
||||
err := os.Setenv("GF_DATA_PATH", "/tmp/env_override")
|
||||
require.NoError(t, err)
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.DataPath, ShouldEqual, "/tmp/env_override")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("instance_name default to hostname even if hostname env is empty", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
So(err, ShouldBeNil)
|
||||
So(InstanceName, ShouldEqual, hostname)
|
||||
})
|
||||
|
||||
Convey("Reading callback_url should add trailing slash", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.RendererCallbackUrl, ShouldEqual, "http://myserver/renderer/")
|
||||
})
|
||||
|
||||
Convey("Only sync_ttl should return the value sync_ttl", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.sync_ttl=2"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.AuthProxySyncTTL, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Only ldap_sync_ttl should return the value ldap_sync_ttl", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.ldap_sync_ttl=5"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.AuthProxySyncTTL, ShouldEqual, 5)
|
||||
})
|
||||
|
||||
Convey("ldap_sync should override ldap_sync_ttl that is default value", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.sync_ttl=5"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.AuthProxySyncTTL, ShouldEqual, 5)
|
||||
})
|
||||
|
||||
Convey("ldap_sync should not override ldap_sync_ttl that is different from default value", func() {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.ldap_sync_ttl=12", "cfg:auth.proxy.sync_ttl=5"},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cfg.AuthProxySyncTTL, ShouldEqual, 12)
|
||||
})
|
||||
require.Equal(t, "admin", cfg.AdminUser)
|
||||
require.Equal(t, "http://localhost:3000/", cfg.RendererCallbackUrl)
|
||||
})
|
||||
|
||||
Convey("Test reading string values from .ini file", t, func() {
|
||||
iniFile, err := ini.Load(path.Join(HomePath, "pkg/setting/testdata/invalid.ini"))
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("default.ini should have no semi-colon commented entries", func(t *testing.T) {
|
||||
file, err := os.Open("../../conf/defaults.ini")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load defaults.ini file: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
require.Nil(t, err)
|
||||
}()
|
||||
|
||||
Convey("If key is found - should return value from ini file", func() {
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
// This only catches values commented out with ";" and will not catch those that are commented out with "#".
|
||||
if strings.HasPrefix(scanner.Text(), ";") {
|
||||
t.Errorf("entries in defaults.ini must not be commented or environment variables will not work: %v", scanner.Text())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sample.ini should load successfully", func(t *testing.T) {
|
||||
customInitPath := CustomInitPath
|
||||
CustomInitPath = "conf/sample.ini"
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
require.Nil(t, err)
|
||||
// Restore CustomInitPath to avoid side effects.
|
||||
CustomInitPath = customInitPath
|
||||
})
|
||||
|
||||
t.Run("Should be able to override via environment variables", func(t *testing.T) {
|
||||
err := os.Setenv("GF_SECURITY_ADMIN_USER", "superduper")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "superduper", cfg.AdminUser)
|
||||
require.Equal(t, filepath.Join(HomePath, "data"), cfg.DataPath)
|
||||
require.Equal(t, filepath.Join(cfg.DataPath, "log"), cfg.LogsPath)
|
||||
})
|
||||
|
||||
t.Run("Should replace password when defined in environment", func(t *testing.T) {
|
||||
err := os.Setenv("GF_SECURITY_ADMIN_PASSWORD", "supersecret")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Contains(t, appliedEnvOverrides, "GF_SECURITY_ADMIN_PASSWORD=*********")
|
||||
})
|
||||
|
||||
t.Run("Should replace password in URL when url environment is defined", func(t *testing.T) {
|
||||
err := os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{HomePath: "../../"})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Contains(t, appliedEnvOverrides, "GF_DATABASE_URL=mysql://user:xxxxx@localhost:3306/database")
|
||||
})
|
||||
|
||||
t.Run("Should get property map from command line args array", func(t *testing.T) {
|
||||
props := getCommandLineProperties([]string{"cfg:test=value", "cfg:map.test=1"})
|
||||
|
||||
require.Equal(t, 2, len(props))
|
||||
require.Equal(t, "value", props["test"])
|
||||
require.Equal(t, "1", props["map.test"])
|
||||
})
|
||||
|
||||
t.Run("Should be able to override via command line", func(t *testing.T) {
|
||||
if runtime.GOOS == windows {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{`cfg:paths.data=c:\tmp\data`, `cfg:paths.logs=c:\tmp\logs`},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, `c:\tmp\data`, cfg.DataPath)
|
||||
require.Equal(t, `c:\tmp\logs`, cfg.LogsPath)
|
||||
} else {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "/tmp/data", cfg.DataPath)
|
||||
require.Equal(t, "/tmp/logs", cfg.LogsPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should be able to override defaults via command line", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{
|
||||
"cfg:default.server.domain=test2",
|
||||
},
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "test2", cfg.Domain)
|
||||
})
|
||||
|
||||
t.Run("Defaults can be overridden in specified config file", func(t *testing.T) {
|
||||
if runtime.GOOS == windows {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"),
|
||||
Args: []string{`cfg:default.paths.data=c:\tmp\data`},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, `c:\tmp\override`, cfg.DataPath)
|
||||
} else {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
|
||||
Args: []string{"cfg:default.paths.data=/tmp/data"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "/tmp/override", cfg.DataPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Command line overrides specified config file", func(t *testing.T) {
|
||||
if runtime.GOOS == windows {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"),
|
||||
Args: []string{`cfg:paths.data=c:\tmp\data`},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, `c:\tmp\data`, cfg.DataPath)
|
||||
} else {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
|
||||
Args: []string{"cfg:paths.data=/tmp/data"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "/tmp/data", cfg.DataPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Can use environment variables in config values", func(t *testing.T) {
|
||||
if runtime.GOOS == windows {
|
||||
err := os.Setenv("GF_DATA_PATH", `c:\tmp\env_override`)
|
||||
require.NoError(t, err)
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, `c:\tmp\env_override`, cfg.DataPath)
|
||||
} else {
|
||||
err := os.Setenv("GF_DATA_PATH", "/tmp/env_override")
|
||||
require.NoError(t, err)
|
||||
cfg := NewCfg()
|
||||
err = cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "/tmp/env_override", cfg.DataPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("instance_name default to hostname even if hostname env is empty", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, hostname, InstanceName)
|
||||
})
|
||||
|
||||
t.Run("Reading callback_url should add trailing slash", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "http://myserver/renderer/", cfg.RendererCallbackUrl)
|
||||
})
|
||||
|
||||
t.Run("Only sync_ttl should return the value sync_ttl", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.sync_ttl=2"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, 2, cfg.AuthProxySyncTTL)
|
||||
})
|
||||
|
||||
t.Run("Only ldap_sync_ttl should return the value ldap_sync_ttl", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.ldap_sync_ttl=5"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, 5, cfg.AuthProxySyncTTL)
|
||||
})
|
||||
|
||||
t.Run("ldap_sync should override ldap_sync_ttl that is default value", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.sync_ttl=5"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, 5, cfg.AuthProxySyncTTL)
|
||||
})
|
||||
|
||||
t.Run("ldap_sync should not override ldap_sync_ttl that is different from default value", func(t *testing.T) {
|
||||
cfg := NewCfg()
|
||||
err := cfg.Load(CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:auth.proxy.ldap_sync_ttl=12", "cfg:auth.proxy.sync_ttl=5"},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, 12, cfg.AuthProxySyncTTL)
|
||||
})
|
||||
|
||||
t.Run("Test reading string values from .ini file", func(t *testing.T) {
|
||||
iniFile, err := ini.Load(path.Join(HomePath, "pkg/setting/testdata/invalid.ini"))
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("If key is found - should return value from ini file", func(t *testing.T) {
|
||||
value := valueAsString(iniFile.Section("server"), "alt_url", "")
|
||||
So(value, ShouldEqual, "https://grafana.com/")
|
||||
require.Equal(t, "https://grafana.com/", value)
|
||||
})
|
||||
|
||||
Convey("If key is not found - should return default value", func() {
|
||||
t.Run("If key is not found - should return default value", func(t *testing.T) {
|
||||
value := valueAsString(iniFile.Section("server"), "extra_url", "default_url_val")
|
||||
So(value, ShouldEqual, "default_url_val")
|
||||
require.Equal(t, "default_url_val", value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1405,7 +1405,23 @@ var expAvailableChannelJsonOutput = `
|
||||
"required": false,
|
||||
"validationRule": "",
|
||||
"secure": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"element": "checkbox",
|
||||
"inputType": "",
|
||||
"label": "Use Discord's Webhook Username",
|
||||
"description": "Use the username configured in Discord's webhook settings. Otherwise, the username will be 'Grafana'",
|
||||
"placeholder": "",
|
||||
"propertyName": "use_discord_username",
|
||||
"selectOptions": null,
|
||||
"showWhen": {
|
||||
"field": "",
|
||||
"is": ""
|
||||
},
|
||||
"required": false,
|
||||
"validationRule": "",
|
||||
"secure": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,15 +8,13 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
Convey("ApplicationInsightsDatasource", t, func() {
|
||||
t.Run("ApplicationInsightsDatasource", func(t *testing.T) {
|
||||
datasource := &ApplicationInsightsDatasource{}
|
||||
|
||||
Convey("Parse queries from frontend and build AzureMonitor API queries", func() {
|
||||
t.Run("Parse queries from frontend and build AzureMonitor API queries", func(t *testing.T) {
|
||||
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
|
||||
tsdbQuery := []backend.DataQuery{
|
||||
{
|
||||
@@ -38,22 +36,22 @@ func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
Interval: 1234,
|
||||
},
|
||||
}
|
||||
Convey("and is a normal query", func() {
|
||||
t.Run("and is a normal query", func(t *testing.T) {
|
||||
queries, err := datasource.buildQueries(tsdbQuery)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(len(queries), ShouldEqual, 1)
|
||||
So(queries[0].RefID, ShouldEqual, "A")
|
||||
So(queries[0].ApiURL, ShouldEqual, "metrics/server/exceptions")
|
||||
So(queries[0].Target, ShouldEqual, "aggregation=Average&interval=PT1M×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
So(len(queries[0].Params), ShouldEqual, 3)
|
||||
So(queries[0].Params["timespan"][0], ShouldEqual, "2018-03-15T13:00:00Z/2018-03-15T13:34:00Z")
|
||||
So(queries[0].Params["aggregation"][0], ShouldEqual, "Average")
|
||||
So(queries[0].Params["interval"][0], ShouldEqual, "PT1M")
|
||||
So(queries[0].Alias, ShouldEqual, "testalias")
|
||||
require.Equal(t, len(queries), 1)
|
||||
require.Equal(t, queries[0].RefID, "A")
|
||||
require.Equal(t, queries[0].ApiURL, "metrics/server/exceptions")
|
||||
require.Equal(t, queries[0].Target, "aggregation=Average&interval=PT1M×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
require.Equal(t, len(queries[0].Params), 3)
|
||||
require.Equal(t, queries[0].Params["timespan"][0], "2018-03-15T13:00:00Z/2018-03-15T13:34:00Z")
|
||||
require.Equal(t, queries[0].Params["aggregation"][0], "Average")
|
||||
require.Equal(t, queries[0].Params["interval"][0], "PT1M")
|
||||
require.Equal(t, queries[0].Alias, "testalias")
|
||||
})
|
||||
|
||||
Convey("and has a time grain set to auto", func() {
|
||||
t.Run("and has a time grain set to auto", func(t *testing.T) {
|
||||
tsdbQuery[0].JSON = []byte(`{
|
||||
"appInsights": {
|
||||
"rawQuery": false,
|
||||
@@ -69,12 +67,12 @@ func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
queries, err := datasource.buildQueries(tsdbQuery)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(queries[0].Params["interval"][0], ShouldEqual, "PT15M")
|
||||
require.Equal(t, queries[0].Params["interval"][0], "PT15M")
|
||||
})
|
||||
|
||||
Convey("and has an empty time grain", func() {
|
||||
t.Run("and has an empty time grain", func(t *testing.T) {
|
||||
tsdbQuery[0].JSON = []byte(`{
|
||||
"appInsights": {
|
||||
"rawQuery": false,
|
||||
@@ -88,12 +86,12 @@ func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
tsdbQuery[0].Interval, _ = time.ParseDuration("400s")
|
||||
|
||||
queries, err := datasource.buildQueries(tsdbQuery)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(queries[0].Params["interval"][0], ShouldEqual, "PT15M")
|
||||
require.Equal(t, queries[0].Params["interval"][0], "PT15M")
|
||||
})
|
||||
|
||||
Convey("and has a time grain set to auto and the metric has a limited list of allowed time grains", func() {
|
||||
t.Run("and has a time grain set to auto and the metric has a limited list of allowed time grains", func(t *testing.T) {
|
||||
tsdbQuery[0].JSON = []byte(`{
|
||||
"appInsights": {
|
||||
"rawQuery": false,
|
||||
@@ -108,12 +106,12 @@ func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
tsdbQuery[0].Interval, _ = time.ParseDuration("400s")
|
||||
|
||||
queries, err := datasource.buildQueries(tsdbQuery)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(queries[0].Params["interval"][0], ShouldEqual, "PT5M")
|
||||
require.Equal(t, queries[0].Params["interval"][0], "PT5M")
|
||||
})
|
||||
|
||||
Convey("and has a dimension filter", func() {
|
||||
t.Run("and has a dimension filter", func(t *testing.T) {
|
||||
tsdbQuery[0].JSON = []byte(`{
|
||||
"appInsights": {
|
||||
"rawQuery": false,
|
||||
@@ -128,13 +126,13 @@ func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
}`)
|
||||
|
||||
queries, err := datasource.buildQueries(tsdbQuery)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(queries[0].Target, ShouldEqual, "aggregation=Average&filter=blob+eq+%27%2A%27&interval=PT1M&segment=blob×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
So(queries[0].Params["filter"][0], ShouldEqual, "blob eq '*'")
|
||||
require.Equal(t, queries[0].Target, "aggregation=Average&filter=blob+eq+%27%2A%27&interval=PT1M&segment=blob×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
require.Equal(t, queries[0].Params["filter"][0], "blob eq '*'")
|
||||
})
|
||||
|
||||
Convey("and has a dimension filter set to None", func() {
|
||||
t.Run("and has a dimension filter set to None", func(t *testing.T) {
|
||||
tsdbQuery[0].JSON = []byte(`{
|
||||
"appInsights": {
|
||||
"rawQuery": false,
|
||||
@@ -148,9 +146,9 @@ func TestApplicationInsightsDatasource(t *testing.T) {
|
||||
}`)
|
||||
|
||||
queries, err := datasource.buildQueries(tsdbQuery)
|
||||
So(err, ShouldBeNil)
|
||||
require.NoError(t, err)
|
||||
|
||||
So(queries[0].Target, ShouldEqual, "aggregation=Average&interval=PT1M×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
require.Equal(t, queries[0].Target, "aggregation=Average&interval=PT1M×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,12 +3,12 @@ package azuremonitor
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestURLBuilder(t *testing.T) {
|
||||
Convey("AzureMonitor URL Builder", t, func() {
|
||||
Convey("when metric definition is in the short form", func() {
|
||||
t.Run("AzureMonitor URL Builder", func(t *testing.T) {
|
||||
t.Run("when metric definition is in the short form", func(t *testing.T) {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
@@ -17,10 +17,10 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is in the short form and a subscription is defined", func() {
|
||||
t.Run("when metric definition is in the short form and a subscription is defined", func(t *testing.T) {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
Subscription: "specified-sub",
|
||||
@@ -30,10 +30,10 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
require.Equal(t, url, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func() {
|
||||
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
@@ -42,10 +42,10 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
|
||||
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func() {
|
||||
t.Run("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func(t *testing.T) {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
@@ -54,10 +54,10 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
|
||||
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func() {
|
||||
t.Run("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func(t *testing.T) {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
@@ -66,7 +66,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/providers/microsoft.insights/metrics")
|
||||
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/providers/microsoft.insights/metrics")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -63,6 +62,8 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
dsName = "stackdriver"
|
||||
|
||||
gceAuthentication string = "gce"
|
||||
jwtAuthentication string = "jwt"
|
||||
metricQueryType string = "metrics"
|
||||
@@ -87,7 +88,7 @@ func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, pl
|
||||
QueryDataHandler: s,
|
||||
})
|
||||
|
||||
if err := s.backendPluginManager.Register("stackdriver", factory); err != nil {
|
||||
if err := s.backendPluginManager.Register(dsName, factory); err != nil {
|
||||
slog.Error("Failed to register plugin", "error", err)
|
||||
}
|
||||
return s
|
||||
@@ -112,9 +113,10 @@ type datasourceInfo struct {
|
||||
url string
|
||||
authenticationType string
|
||||
defaultProject string
|
||||
clientEmail string
|
||||
tokenUri string
|
||||
client *http.Client
|
||||
|
||||
jsonData map[string]interface{}
|
||||
decryptedSecureJSONData map[string]string
|
||||
}
|
||||
|
||||
@@ -126,16 +128,6 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
return nil, fmt.Errorf("error reading settings: %w", err)
|
||||
}
|
||||
|
||||
opts, err := settings.HTTPClientOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := httpClientProvider.New(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authType := jwtAuthentication
|
||||
if authTypeOverride, ok := jsonData["authenticationType"].(string); ok && authTypeOverride != "" {
|
||||
authType = authTypeOverride
|
||||
@@ -146,16 +138,38 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
defaultProject = jsonData["defaultProject"].(string)
|
||||
}
|
||||
|
||||
return &datasourceInfo{
|
||||
var clientEmail string
|
||||
if jsonData["clientEmail"] != nil {
|
||||
clientEmail = jsonData["clientEmail"].(string)
|
||||
}
|
||||
|
||||
var tokenUri string
|
||||
if jsonData["tokenUri"] != nil {
|
||||
tokenUri = jsonData["tokenUri"].(string)
|
||||
}
|
||||
|
||||
dsInfo := &datasourceInfo{
|
||||
id: settings.ID,
|
||||
updated: settings.Updated,
|
||||
url: settings.URL,
|
||||
authenticationType: authType,
|
||||
defaultProject: defaultProject,
|
||||
client: client,
|
||||
jsonData: jsonData,
|
||||
clientEmail: clientEmail,
|
||||
tokenUri: tokenUri,
|
||||
decryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
opts, err := settings.HTTPClientOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsInfo.client, err = newHTTPClient(dsInfo, opts, httpClientProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dsInfo, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,14 +354,6 @@ func (s *Service) buildQueryExecutors(req *backend.QueryDataRequest) ([]cloudMon
|
||||
return cloudMonitoringQueryExecutors, nil
|
||||
}
|
||||
|
||||
func reverse(s string) string {
|
||||
chars := []rune(s)
|
||||
for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
|
||||
chars[i], chars[j] = chars[j], chars[i]
|
||||
}
|
||||
return string(chars)
|
||||
}
|
||||
|
||||
func interpolateFilterWildcards(value string) string {
|
||||
matches := strings.Count(value, "*")
|
||||
switch {
|
||||
@@ -478,19 +484,6 @@ func calculateAlignmentPeriod(alignmentPeriod string, intervalMs int64, duration
|
||||
return alignmentPeriod
|
||||
}
|
||||
|
||||
func toSnakeCase(str string) string {
|
||||
return strings.ToLower(matchAllCap.ReplaceAllString(str, "${1}_${2}"))
|
||||
}
|
||||
|
||||
func containsLabel(labels []string, newLabel string) bool {
|
||||
for _, val := range labels {
|
||||
if val == newLabel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatLegendKeys(metricType string, defaultMetricName string, labels map[string]string,
|
||||
additionalLabels map[string]string, query *cloudMonitoringTimeSeriesFilter) string {
|
||||
if query.AliasBy == "" {
|
||||
@@ -589,34 +582,14 @@ func (s *Service) createRequest(ctx context.Context, pluginCtx backend.PluginCon
|
||||
if body != nil {
|
||||
method = http.MethodPost
|
||||
}
|
||||
req, err := http.NewRequest(method, "https://monitoring.googleapis.com/", body)
|
||||
req, err := http.NewRequest(method, cloudMonitoringRoute.url, body)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create request", "error", err)
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// find plugin
|
||||
plugin := s.pluginManager.GetDataSource(pluginCtx.PluginID)
|
||||
if plugin == nil {
|
||||
return nil, errors.New("unable to find datasource plugin CloudMonitoring")
|
||||
}
|
||||
|
||||
var cloudMonitoringRoute *plugins.AppPluginRoute
|
||||
for _, route := range plugin.Routes {
|
||||
if route.Path == "cloudmonitoring" {
|
||||
cloudMonitoringRoute = route
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pluginproxy.ApplyRoute(ctx, req, proxyPass, cloudMonitoringRoute, pluginproxy.DSInfo{
|
||||
ID: dsInfo.id,
|
||||
Updated: dsInfo.updated,
|
||||
JSONData: dsInfo.jsonData,
|
||||
DecryptedSecureJSONData: dsInfo.decryptedSecureJSONData,
|
||||
}, s.cfg)
|
||||
req.URL.Path = proxyPass
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package cloudmonitoring
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-google-sdk-go/pkg/tokenprovider"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
infrahttp "github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
)
|
||||
|
||||
var cloudMonitoringRoute = struct {
|
||||
path string
|
||||
method string
|
||||
url string
|
||||
scopes []string
|
||||
}{
|
||||
path: "cloudmonitoring",
|
||||
method: "GET",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
scopes: []string{"https://www.googleapis.com/auth/monitoring.read"},
|
||||
}
|
||||
|
||||
func getMiddleware(model *datasourceInfo) (httpclient.Middleware, error) {
|
||||
providerConfig := tokenprovider.Config{
|
||||
RoutePath: cloudMonitoringRoute.path,
|
||||
RouteMethod: cloudMonitoringRoute.method,
|
||||
DataSourceID: model.id,
|
||||
DataSourceUpdated: model.updated,
|
||||
Scopes: cloudMonitoringRoute.scopes,
|
||||
}
|
||||
|
||||
var provider tokenprovider.TokenProvider
|
||||
switch model.authenticationType {
|
||||
case gceAuthentication:
|
||||
provider = tokenprovider.NewGceAccessTokenProvider(providerConfig)
|
||||
case jwtAuthentication:
|
||||
providerConfig.JwtTokenConfig = &tokenprovider.JwtTokenConfig{
|
||||
Email: model.clientEmail,
|
||||
URI: model.tokenUri,
|
||||
PrivateKey: []byte(model.decryptedSecureJSONData["privateKey"]),
|
||||
}
|
||||
provider = tokenprovider.NewJwtAccessTokenProvider(providerConfig)
|
||||
}
|
||||
|
||||
return tokenprovider.AuthMiddleware(provider), nil
|
||||
}
|
||||
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider infrahttp.Provider) (*http.Client, error) {
|
||||
m, err := getMiddleware(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts.Middlewares = append(opts.Middlewares, m)
|
||||
return clientProvider.New(opts)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) run(ctx context.Context
|
||||
slog.Info("No project name set on query, using project name from datasource", "projectName", projectName)
|
||||
}
|
||||
|
||||
r, err := s.createRequest(ctx, req.PluginContext, &dsInfo, path.Join("cloudmonitoringv3/projects", projectName, "timeSeries"), nil)
|
||||
r, err := s.createRequest(ctx, req.PluginContext, &dsInfo, path.Join("/v3/projects", projectName, "timeSeries"), nil)
|
||||
if err != nil {
|
||||
dr.Error = err
|
||||
return dr, cloudMonitoringResponse{}, "", nil
|
||||
|
||||
@@ -49,7 +49,7 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, r
|
||||
dr.Error = err
|
||||
return dr, cloudMonitoringResponse{}, "", nil
|
||||
}
|
||||
r, err := s.createRequest(ctx, req.PluginContext, &dsInfo, path.Join("cloudmonitoringv3/projects", projectName, "timeSeries:query"), bytes.NewBuffer(buf))
|
||||
r, err := s.createRequest(ctx, req.PluginContext, &dsInfo, path.Join("/v3/projects", projectName, "timeSeries:query"), bytes.NewBuffer(buf))
|
||||
if err != nil {
|
||||
dr.Error = err
|
||||
return dr, cloudMonitoringResponse{}, "", nil
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package cloudmonitoring
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func reverse(s string) string {
|
||||
chars := []rune(s)
|
||||
for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
|
||||
chars[i], chars[j] = chars[j], chars[i]
|
||||
}
|
||||
return string(chars)
|
||||
}
|
||||
|
||||
func toSnakeCase(str string) string {
|
||||
return strings.ToLower(matchAllCap.ReplaceAllString(str, "${1}_${2}"))
|
||||
}
|
||||
|
||||
func containsLabel(labels []string, newLabel string) bool {
|
||||
for _, val := range labels {
|
||||
if val == newLabel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -8,437 +8,445 @@ import (
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSearchRequest(t *testing.T) {
|
||||
Convey("Test elasticsearch search request", t, func() {
|
||||
timeField := "@timestamp"
|
||||
Convey("Given new search request builder for es version 5", func() {
|
||||
version5, _ := semver.NewVersion("5.0.0")
|
||||
b := NewSearchRequestBuilder(version5, intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
timeField := "@timestamp"
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
setup := func() *SearchRequestBuilder {
|
||||
version5, _ := semver.NewVersion("5.0.0")
|
||||
return NewSearchRequestBuilder(version5, intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
}
|
||||
|
||||
Convey("Should have size of zero", func() {
|
||||
So(sr.Size, ShouldEqual, 0)
|
||||
})
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
b := setup()
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("Should have no sorting", func() {
|
||||
So(sr.Sort, ShouldHaveLength, 0)
|
||||
})
|
||||
t.Run("Should have size of zero", func(t *testing.T) {
|
||||
require.Equal(t, 0, sr.Size)
|
||||
})
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
So(json.Get("size").MustInt(500), ShouldEqual, 0)
|
||||
So(json.Get("sort").Interface(), ShouldBeNil)
|
||||
So(json.Get("aggs").Interface(), ShouldBeNil)
|
||||
So(json.Get("query").Interface(), ShouldBeNil)
|
||||
})
|
||||
t.Run("Should have no sorting", func(t *testing.T) {
|
||||
require.Equal(t, 0, len(sr.Sort))
|
||||
})
|
||||
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, json.Get("size").MustInt(500))
|
||||
require.Nil(t, json.Get("sort").Interface())
|
||||
require.Nil(t, json.Get("aggs").Interface())
|
||||
require.Nil(t, json.Get("query").Interface())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("When adding size, sort, filters", func(t *testing.T) {
|
||||
b := setup()
|
||||
b.Size(200)
|
||||
b.SortDesc(timeField, "boolean")
|
||||
filters := b.Query().Bool().Filter()
|
||||
filters.AddDateRangeFilter(timeField, "$timeTo", "$timeFrom", DateFormatEpochMS)
|
||||
filters.AddQueryStringFilter("test", true)
|
||||
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("Should have correct size", func(t *testing.T) {
|
||||
require.Equal(t, 200, sr.Size)
|
||||
})
|
||||
|
||||
Convey("When adding size, sort, filters", func() {
|
||||
b.Size(200)
|
||||
b.SortDesc(timeField, "boolean")
|
||||
filters := b.Query().Bool().Filter()
|
||||
filters.AddDateRangeFilter(timeField, "$timeTo", "$timeFrom", DateFormatEpochMS)
|
||||
filters.AddQueryStringFilter("test", true)
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should have correct size", func() {
|
||||
So(sr.Size, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("Should have correct sorting", func() {
|
||||
sort, ok := sr.Sort[timeField].(map[string]string)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(sort["order"], ShouldEqual, "desc")
|
||||
So(sort["unmapped_type"], ShouldEqual, "boolean")
|
||||
})
|
||||
|
||||
Convey("Should have range filter", func() {
|
||||
f, ok := sr.Query.Bool.Filters[0].(*RangeFilter)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(f.Gte, ShouldEqual, "$timeFrom")
|
||||
So(f.Lte, ShouldEqual, "$timeTo")
|
||||
So(f.Format, ShouldEqual, "epoch_millis")
|
||||
})
|
||||
|
||||
Convey("Should have query string filter", func() {
|
||||
f, ok := sr.Query.Bool.Filters[1].(*QueryStringFilter)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(f.Query, ShouldEqual, "test")
|
||||
So(f.AnalyzeWildcard, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
So(json.Get("size").MustInt(0), ShouldEqual, 200)
|
||||
|
||||
sort := json.GetPath("sort", timeField)
|
||||
So(sort.Get("order").MustString(), ShouldEqual, "desc")
|
||||
So(sort.Get("unmapped_type").MustString(), ShouldEqual, "boolean")
|
||||
|
||||
timeRangeFilter := json.GetPath("query", "bool", "filter").GetIndex(0).Get("range").Get(timeField)
|
||||
So(timeRangeFilter.Get("gte").MustString(""), ShouldEqual, "$timeFrom")
|
||||
So(timeRangeFilter.Get("lte").MustString(""), ShouldEqual, "$timeTo")
|
||||
So(timeRangeFilter.Get("format").MustString(""), ShouldEqual, DateFormatEpochMS)
|
||||
|
||||
queryStringFilter := json.GetPath("query", "bool", "filter").GetIndex(1).Get("query_string")
|
||||
So(queryStringFilter.Get("analyze_wildcard").MustBool(false), ShouldEqual, true)
|
||||
So(queryStringFilter.Get("query").MustString(""), ShouldEqual, "test")
|
||||
})
|
||||
})
|
||||
t.Run("Should have correct sorting", func(t *testing.T) {
|
||||
sort, ok := sr.Sort[timeField].(map[string]string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "desc", sort["order"])
|
||||
require.Equal(t, "boolean", sort["unmapped_type"])
|
||||
})
|
||||
|
||||
Convey("When adding doc value field", func() {
|
||||
b.AddDocValueField(timeField)
|
||||
|
||||
Convey("should set correct props", func() {
|
||||
So(b.customProps["fields"], ShouldBeNil)
|
||||
|
||||
scriptFields, ok := b.customProps["script_fields"].(map[string]interface{})
|
||||
So(ok, ShouldBeTrue)
|
||||
So(scriptFields, ShouldHaveLength, 0)
|
||||
|
||||
docValueFields, ok := b.customProps["docvalue_fields"].([]string)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(docValueFields, ShouldHaveLength, 1)
|
||||
So(docValueFields[0], ShouldEqual, timeField)
|
||||
})
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
scriptFields, err := json.Get("script_fields").Map()
|
||||
So(err, ShouldBeNil)
|
||||
So(scriptFields, ShouldHaveLength, 0)
|
||||
|
||||
_, err = json.Get("fields").StringArray()
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
docValueFields, err := json.Get("docvalue_fields").StringArray()
|
||||
So(err, ShouldBeNil)
|
||||
So(docValueFields, ShouldHaveLength, 1)
|
||||
So(docValueFields[0], ShouldEqual, timeField)
|
||||
})
|
||||
})
|
||||
t.Run("Should have range filter", func(t *testing.T) {
|
||||
f, ok := sr.Query.Bool.Filters[0].(*RangeFilter)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "$timeFrom", f.Gte)
|
||||
require.Equal(t, "$timeTo", f.Lte)
|
||||
require.Equal(t, "epoch_millis", f.Format)
|
||||
})
|
||||
|
||||
Convey("and adding multiple top level aggs", func() {
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", nil)
|
||||
aggBuilder.DateHistogram("2", "@timestamp", nil)
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should have 2 top level aggs", func() {
|
||||
aggs := sr.Aggs
|
||||
So(aggs, ShouldHaveLength, 2)
|
||||
So(aggs[0].Key, ShouldEqual, "1")
|
||||
So(aggs[0].Aggregation.Type, ShouldEqual, "terms")
|
||||
So(aggs[1].Key, ShouldEqual, "2")
|
||||
So(aggs[1].Aggregation.Type, ShouldEqual, "date_histogram")
|
||||
})
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(json.Get("aggs").MustMap(), ShouldHaveLength, 2)
|
||||
So(json.GetPath("aggs", "1", "terms", "field").MustString(), ShouldEqual, "@hostname")
|
||||
So(json.GetPath("aggs", "2", "date_histogram", "field").MustString(), ShouldEqual, "@timestamp")
|
||||
})
|
||||
})
|
||||
t.Run("Should have query string filter", func(t *testing.T) {
|
||||
f, ok := sr.Query.Bool.Filters[1].(*QueryStringFilter)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "test", f.Query)
|
||||
require.True(t, f.AnalyzeWildcard)
|
||||
})
|
||||
|
||||
Convey("and adding top level agg with child agg", func() {
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.DateHistogram("2", "@timestamp", nil)
|
||||
})
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 200, json.Get("size").MustInt(0))
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
sort := json.GetPath("sort", timeField)
|
||||
require.Equal(t, "desc", sort.Get("order").MustString())
|
||||
require.Equal(t, "boolean", sort.Get("unmapped_type").MustString())
|
||||
|
||||
Convey("Should have 1 top level agg and one child agg", func() {
|
||||
aggs := sr.Aggs
|
||||
So(aggs, ShouldHaveLength, 1)
|
||||
timeRangeFilter := json.GetPath("query", "bool", "filter").GetIndex(0).Get("range").Get(timeField)
|
||||
require.Equal(t, "$timeFrom", timeRangeFilter.Get("gte").MustString(""))
|
||||
require.Equal(t, "$timeTo", timeRangeFilter.Get("lte").MustString(""))
|
||||
require.Equal(t, DateFormatEpochMS, timeRangeFilter.Get("format").MustString(""))
|
||||
|
||||
topAgg := aggs[0]
|
||||
So(topAgg.Key, ShouldEqual, "1")
|
||||
So(topAgg.Aggregation.Type, ShouldEqual, "terms")
|
||||
So(topAgg.Aggregation.Aggs, ShouldHaveLength, 1)
|
||||
queryStringFilter := json.GetPath("query", "bool", "filter").GetIndex(1).Get("query_string")
|
||||
require.Equal(t, true, queryStringFilter.Get("analyze_wildcard").MustBool(false))
|
||||
require.Equal(t, "test", queryStringFilter.Get("query").MustString(""))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
childAgg := aggs[0].Aggregation.Aggs[0]
|
||||
So(childAgg.Key, ShouldEqual, "2")
|
||||
So(childAgg.Aggregation.Type, ShouldEqual, "date_histogram")
|
||||
})
|
||||
t.Run("When adding doc value field", func(t *testing.T) {
|
||||
b := setup()
|
||||
b.AddDocValueField(timeField)
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("should set correct props", func(t *testing.T) {
|
||||
require.Nil(t, b.customProps["fields"])
|
||||
|
||||
So(json.Get("aggs").MustMap(), ShouldHaveLength, 1)
|
||||
firstLevelAgg := json.GetPath("aggs", "1")
|
||||
secondLevelAgg := firstLevelAgg.GetPath("aggs", "2")
|
||||
So(firstLevelAgg.GetPath("terms", "field").MustString(), ShouldEqual, "@hostname")
|
||||
So(secondLevelAgg.GetPath("date_histogram", "field").MustString(), ShouldEqual, "@timestamp")
|
||||
})
|
||||
})
|
||||
scriptFields, ok := b.customProps["script_fields"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 0, len(scriptFields))
|
||||
|
||||
docValueFields, ok := b.customProps["docvalue_fields"].([]string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, len(docValueFields))
|
||||
require.Equal(t, timeField, docValueFields[0])
|
||||
})
|
||||
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
scriptFields, err := json.Get("script_fields").Map()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(scriptFields))
|
||||
|
||||
_, err = json.Get("fields").StringArray()
|
||||
require.Error(t, err)
|
||||
|
||||
docValueFields, err := json.Get("docvalue_fields").StringArray()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(docValueFields))
|
||||
require.Equal(t, timeField, docValueFields[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("and adding multiple top level aggs", func(t *testing.T) {
|
||||
b := setup()
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", nil)
|
||||
aggBuilder.DateHistogram("2", "@timestamp", nil)
|
||||
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("Should have 2 top level aggs", func(t *testing.T) {
|
||||
aggs := sr.Aggs
|
||||
require.Equal(t, 2, len(aggs))
|
||||
require.Equal(t, "1", aggs[0].Key)
|
||||
require.Equal(t, "terms", aggs[0].Aggregation.Type)
|
||||
require.Equal(t, "2", aggs[1].Key)
|
||||
require.Equal(t, "date_histogram", aggs[1].Aggregation.Type)
|
||||
})
|
||||
|
||||
Convey("and adding two top level aggs with child agg", func() {
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Histogram("1", "@hostname", func(a *HistogramAgg, ib AggBuilder) {
|
||||
ib.DateHistogram("2", "@timestamp", nil)
|
||||
})
|
||||
aggBuilder.Filters("3", func(a *FiltersAggregation, ib AggBuilder) {
|
||||
ib.Terms("4", "@test", nil)
|
||||
})
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
require.Equal(t, 2, len(json.Get("aggs").MustMap()))
|
||||
require.Equal(t, "@hostname", json.GetPath("aggs", "1", "terms", "field").MustString())
|
||||
require.Equal(t, "@timestamp", json.GetPath("aggs", "2", "date_histogram", "field").MustString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should have 2 top level aggs with one child agg each", func() {
|
||||
aggs := sr.Aggs
|
||||
So(aggs, ShouldHaveLength, 2)
|
||||
t.Run("and adding top level agg with child agg", func(t *testing.T) {
|
||||
b := setup()
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.DateHistogram("2", "@timestamp", nil)
|
||||
})
|
||||
|
||||
topAggOne := aggs[0]
|
||||
So(topAggOne.Key, ShouldEqual, "1")
|
||||
So(topAggOne.Aggregation.Type, ShouldEqual, "histogram")
|
||||
So(topAggOne.Aggregation.Aggs, ShouldHaveLength, 1)
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
topAggOnechildAgg := topAggOne.Aggregation.Aggs[0]
|
||||
So(topAggOnechildAgg.Key, ShouldEqual, "2")
|
||||
So(topAggOnechildAgg.Aggregation.Type, ShouldEqual, "date_histogram")
|
||||
t.Run("Should have 1 top level agg and one child agg", func(t *testing.T) {
|
||||
aggs := sr.Aggs
|
||||
require.Equal(t, 1, len(aggs))
|
||||
|
||||
topAggTwo := aggs[1]
|
||||
So(topAggTwo.Key, ShouldEqual, "3")
|
||||
So(topAggTwo.Aggregation.Type, ShouldEqual, "filters")
|
||||
So(topAggTwo.Aggregation.Aggs, ShouldHaveLength, 1)
|
||||
topAgg := aggs[0]
|
||||
require.Equal(t, "1", topAgg.Key)
|
||||
require.Equal(t, "terms", topAgg.Aggregation.Type)
|
||||
require.Equal(t, 1, len(topAgg.Aggregation.Aggs))
|
||||
|
||||
topAggTwochildAgg := topAggTwo.Aggregation.Aggs[0]
|
||||
So(topAggTwochildAgg.Key, ShouldEqual, "4")
|
||||
So(topAggTwochildAgg.Aggregation.Type, ShouldEqual, "terms")
|
||||
})
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
topAggOne := json.GetPath("aggs", "1")
|
||||
So(topAggOne.GetPath("histogram", "field").MustString(), ShouldEqual, "@hostname")
|
||||
topAggOnechildAgg := topAggOne.GetPath("aggs", "2")
|
||||
So(topAggOnechildAgg.GetPath("date_histogram", "field").MustString(), ShouldEqual, "@timestamp")
|
||||
|
||||
topAggTwo := json.GetPath("aggs", "3")
|
||||
topAggTwochildAgg := topAggTwo.GetPath("aggs", "4")
|
||||
So(topAggTwo.GetPath("filters").MustArray(), ShouldHaveLength, 0)
|
||||
So(topAggTwochildAgg.GetPath("terms", "field").MustString(), ShouldEqual, "@test")
|
||||
})
|
||||
})
|
||||
childAgg := aggs[0].Aggregation.Aggs[0]
|
||||
require.Equal(t, "2", childAgg.Key)
|
||||
require.Equal(t, "date_histogram", childAgg.Aggregation.Type)
|
||||
})
|
||||
|
||||
Convey("and adding top level agg with child agg with child agg", func() {
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.Terms("2", "@app", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.DateHistogram("3", "@timestamp", nil)
|
||||
})
|
||||
})
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
require.Equal(t, 1, len(json.Get("aggs").MustMap()))
|
||||
firstLevelAgg := json.GetPath("aggs", "1")
|
||||
secondLevelAgg := firstLevelAgg.GetPath("aggs", "2")
|
||||
require.Equal(t, "@hostname", firstLevelAgg.GetPath("terms", "field").MustString())
|
||||
require.Equal(t, "@timestamp", secondLevelAgg.GetPath("date_histogram", "field").MustString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should have 1 top level agg with one child having a child", func() {
|
||||
aggs := sr.Aggs
|
||||
So(aggs, ShouldHaveLength, 1)
|
||||
t.Run("and adding two top level aggs with child agg", func(t *testing.T) {
|
||||
b := setup()
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Histogram("1", "@hostname", func(a *HistogramAgg, ib AggBuilder) {
|
||||
ib.DateHistogram("2", "@timestamp", nil)
|
||||
})
|
||||
aggBuilder.Filters("3", func(a *FiltersAggregation, ib AggBuilder) {
|
||||
ib.Terms("4", "@test", nil)
|
||||
})
|
||||
|
||||
topAgg := aggs[0]
|
||||
So(topAgg.Key, ShouldEqual, "1")
|
||||
So(topAgg.Aggregation.Type, ShouldEqual, "terms")
|
||||
So(topAgg.Aggregation.Aggs, ShouldHaveLength, 1)
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
childAgg := topAgg.Aggregation.Aggs[0]
|
||||
So(childAgg.Key, ShouldEqual, "2")
|
||||
So(childAgg.Aggregation.Type, ShouldEqual, "terms")
|
||||
t.Run("Should have 2 top level aggs with one child agg each", func(t *testing.T) {
|
||||
aggs := sr.Aggs
|
||||
require.Equal(t, 2, len(aggs))
|
||||
|
||||
childChildAgg := childAgg.Aggregation.Aggs[0]
|
||||
So(childChildAgg.Key, ShouldEqual, "3")
|
||||
So(childChildAgg.Aggregation.Type, ShouldEqual, "date_histogram")
|
||||
})
|
||||
topAggOne := aggs[0]
|
||||
require.Equal(t, "1", topAggOne.Key)
|
||||
require.Equal(t, "histogram", topAggOne.Aggregation.Type)
|
||||
require.Equal(t, 1, len(topAggOne.Aggregation.Aggs))
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
topAggOnechildAgg := topAggOne.Aggregation.Aggs[0]
|
||||
require.Equal(t, "2", topAggOnechildAgg.Key)
|
||||
require.Equal(t, "date_histogram", topAggOnechildAgg.Aggregation.Type)
|
||||
|
||||
topAgg := json.GetPath("aggs", "1")
|
||||
So(topAgg.GetPath("terms", "field").MustString(), ShouldEqual, "@hostname")
|
||||
topAggTwo := aggs[1]
|
||||
require.Equal(t, "3", topAggTwo.Key)
|
||||
require.Equal(t, "filters", topAggTwo.Aggregation.Type)
|
||||
require.Equal(t, 1, len(topAggTwo.Aggregation.Aggs))
|
||||
|
||||
childAgg := topAgg.GetPath("aggs", "2")
|
||||
So(childAgg.GetPath("terms", "field").MustString(), ShouldEqual, "@app")
|
||||
|
||||
childChildAgg := childAgg.GetPath("aggs", "3")
|
||||
So(childChildAgg.GetPath("date_histogram", "field").MustString(), ShouldEqual, "@timestamp")
|
||||
})
|
||||
})
|
||||
topAggTwochildAgg := topAggTwo.Aggregation.Aggs[0]
|
||||
require.Equal(t, "4", topAggTwochildAgg.Key)
|
||||
require.Equal(t, "terms", topAggTwochildAgg.Aggregation.Type)
|
||||
})
|
||||
|
||||
Convey("and adding bucket and metric aggs", func() {
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.Terms("2", "@app", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.Metric("4", "avg", "@value", nil)
|
||||
ib.DateHistogram("3", "@timestamp", func(a *DateHistogramAgg, ib AggBuilder) {
|
||||
ib.Metric("4", "avg", "@value", nil)
|
||||
ib.Metric("5", "max", "@value", nil)
|
||||
})
|
||||
})
|
||||
})
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
topAggOne := json.GetPath("aggs", "1")
|
||||
require.Equal(t, "@hostname", topAggOne.GetPath("histogram", "field").MustString())
|
||||
topAggOnechildAgg := topAggOne.GetPath("aggs", "2")
|
||||
require.Equal(t, "@timestamp", topAggOnechildAgg.GetPath("date_histogram", "field").MustString())
|
||||
|
||||
Convey("Should have 1 top level agg with one child having a child", func() {
|
||||
aggs := sr.Aggs
|
||||
So(aggs, ShouldHaveLength, 1)
|
||||
topAggTwo := json.GetPath("aggs", "3")
|
||||
topAggTwochildAgg := topAggTwo.GetPath("aggs", "4")
|
||||
require.Equal(t, 0, len(topAggTwo.GetPath("filters").MustArray()))
|
||||
require.Equal(t, "@test", topAggTwochildAgg.GetPath("terms", "field").MustString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
topAgg := aggs[0]
|
||||
So(topAgg.Key, ShouldEqual, "1")
|
||||
So(topAgg.Aggregation.Type, ShouldEqual, "terms")
|
||||
So(topAgg.Aggregation.Aggs, ShouldHaveLength, 1)
|
||||
t.Run("and adding top level agg with child agg with child agg", func(t *testing.T) {
|
||||
b := setup()
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.Terms("2", "@app", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.DateHistogram("3", "@timestamp", nil)
|
||||
})
|
||||
})
|
||||
|
||||
childAgg := topAgg.Aggregation.Aggs[0]
|
||||
So(childAgg.Key, ShouldEqual, "2")
|
||||
So(childAgg.Aggregation.Type, ShouldEqual, "terms")
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
childChildOneAgg := childAgg.Aggregation.Aggs[0]
|
||||
So(childChildOneAgg.Key, ShouldEqual, "4")
|
||||
So(childChildOneAgg.Aggregation.Type, ShouldEqual, "avg")
|
||||
t.Run("Should have 1 top level agg with one child having a child", func(t *testing.T) {
|
||||
aggs := sr.Aggs
|
||||
require.Equal(t, 1, len(aggs))
|
||||
|
||||
childChildTwoAgg := childAgg.Aggregation.Aggs[1]
|
||||
So(childChildTwoAgg.Key, ShouldEqual, "3")
|
||||
So(childChildTwoAgg.Aggregation.Type, ShouldEqual, "date_histogram")
|
||||
topAgg := aggs[0]
|
||||
require.Equal(t, "1", topAgg.Key)
|
||||
require.Equal(t, "terms", topAgg.Aggregation.Type)
|
||||
require.Equal(t, 1, len(topAgg.Aggregation.Aggs))
|
||||
|
||||
childChildTwoChildOneAgg := childChildTwoAgg.Aggregation.Aggs[0]
|
||||
So(childChildTwoChildOneAgg.Key, ShouldEqual, "4")
|
||||
So(childChildTwoChildOneAgg.Aggregation.Type, ShouldEqual, "avg")
|
||||
childAgg := topAgg.Aggregation.Aggs[0]
|
||||
require.Equal(t, "2", childAgg.Key)
|
||||
require.Equal(t, "terms", childAgg.Aggregation.Type)
|
||||
|
||||
childChildTwoChildTwoAgg := childChildTwoAgg.Aggregation.Aggs[1]
|
||||
So(childChildTwoChildTwoAgg.Key, ShouldEqual, "5")
|
||||
So(childChildTwoChildTwoAgg.Aggregation.Type, ShouldEqual, "max")
|
||||
})
|
||||
childChildAgg := childAgg.Aggregation.Aggs[0]
|
||||
require.Equal(t, "3", childChildAgg.Key)
|
||||
require.Equal(t, "date_histogram", childChildAgg.Aggregation.Type)
|
||||
})
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
termsAgg := json.GetPath("aggs", "1")
|
||||
So(termsAgg.GetPath("terms", "field").MustString(), ShouldEqual, "@hostname")
|
||||
topAgg := json.GetPath("aggs", "1")
|
||||
require.Equal(t, "@hostname", topAgg.GetPath("terms", "field").MustString())
|
||||
|
||||
termsAggTwo := termsAgg.GetPath("aggs", "2")
|
||||
So(termsAggTwo.GetPath("terms", "field").MustString(), ShouldEqual, "@app")
|
||||
childAgg := topAgg.GetPath("aggs", "2")
|
||||
require.Equal(t, "@app", childAgg.GetPath("terms", "field").MustString())
|
||||
|
||||
termsAggTwoAvg := termsAggTwo.GetPath("aggs", "4")
|
||||
So(termsAggTwoAvg.GetPath("avg", "field").MustString(), ShouldEqual, "@value")
|
||||
childChildAgg := childAgg.GetPath("aggs", "3")
|
||||
require.Equal(t, "@timestamp", childChildAgg.GetPath("date_histogram", "field").MustString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
dateHistAgg := termsAggTwo.GetPath("aggs", "3")
|
||||
So(dateHistAgg.GetPath("date_histogram", "field").MustString(), ShouldEqual, "@timestamp")
|
||||
|
||||
avgAgg := dateHistAgg.GetPath("aggs", "4")
|
||||
So(avgAgg.GetPath("avg", "field").MustString(), ShouldEqual, "@value")
|
||||
|
||||
maxAgg := dateHistAgg.GetPath("aggs", "5")
|
||||
So(maxAgg.GetPath("max", "field").MustString(), ShouldEqual, "@value")
|
||||
})
|
||||
t.Run("and adding bucket and metric aggs", func(t *testing.T) {
|
||||
b := setup()
|
||||
aggBuilder := b.Agg()
|
||||
aggBuilder.Terms("1", "@hostname", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.Terms("2", "@app", func(a *TermsAggregation, ib AggBuilder) {
|
||||
ib.Metric("4", "avg", "@value", nil)
|
||||
ib.DateHistogram("3", "@timestamp", func(a *DateHistogramAgg, ib AggBuilder) {
|
||||
ib.Metric("4", "avg", "@value", nil)
|
||||
ib.Metric("5", "max", "@value", nil)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given new search request builder for es version 2", func() {
|
||||
version2, _ := semver.NewVersion("2.0.0")
|
||||
b := NewSearchRequestBuilder(version2, intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("When adding doc value field", func() {
|
||||
b.AddDocValueField(timeField)
|
||||
t.Run("Should have 1 top level agg with one child having a child", func(t *testing.T) {
|
||||
aggs := sr.Aggs
|
||||
require.Equal(t, 1, len(aggs))
|
||||
|
||||
Convey("should set correct props", func() {
|
||||
fields, ok := b.customProps["fields"].([]string)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(fields, ShouldHaveLength, 2)
|
||||
So(fields[0], ShouldEqual, "*")
|
||||
So(fields[1], ShouldEqual, "_source")
|
||||
topAgg := aggs[0]
|
||||
require.Equal(t, "1", topAgg.Key)
|
||||
require.Equal(t, "terms", topAgg.Aggregation.Type)
|
||||
require.Equal(t, 1, len(topAgg.Aggregation.Aggs))
|
||||
|
||||
scriptFields, ok := b.customProps["script_fields"].(map[string]interface{})
|
||||
So(ok, ShouldBeTrue)
|
||||
So(scriptFields, ShouldHaveLength, 0)
|
||||
childAgg := topAgg.Aggregation.Aggs[0]
|
||||
require.Equal(t, "2", childAgg.Key)
|
||||
require.Equal(t, "terms", childAgg.Aggregation.Type)
|
||||
|
||||
fieldDataFields, ok := b.customProps["fielddata_fields"].([]string)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(fieldDataFields, ShouldHaveLength, 1)
|
||||
So(fieldDataFields[0], ShouldEqual, timeField)
|
||||
})
|
||||
childChildOneAgg := childAgg.Aggregation.Aggs[0]
|
||||
require.Equal(t, "4", childChildOneAgg.Key)
|
||||
require.Equal(t, "avg", childChildOneAgg.Aggregation.Type)
|
||||
|
||||
Convey("When building search request", func() {
|
||||
sr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
childChildTwoAgg := childAgg.Aggregation.Aggs[1]
|
||||
require.Equal(t, "3", childChildTwoAgg.Key)
|
||||
require.Equal(t, "date_histogram", childChildTwoAgg.Aggregation.Type)
|
||||
|
||||
Convey("When marshal to JSON should generate correct json", func() {
|
||||
body, err := json.Marshal(sr)
|
||||
So(err, ShouldBeNil)
|
||||
json, err := simplejson.NewJson(body)
|
||||
So(err, ShouldBeNil)
|
||||
childChildTwoChildOneAgg := childChildTwoAgg.Aggregation.Aggs[0]
|
||||
require.Equal(t, "4", childChildTwoChildOneAgg.Key)
|
||||
require.Equal(t, "avg", childChildTwoChildOneAgg.Aggregation.Type)
|
||||
|
||||
scriptFields, err := json.Get("script_fields").Map()
|
||||
So(err, ShouldBeNil)
|
||||
So(scriptFields, ShouldHaveLength, 0)
|
||||
childChildTwoChildTwoAgg := childChildTwoAgg.Aggregation.Aggs[1]
|
||||
require.Equal(t, "5", childChildTwoChildTwoAgg.Key)
|
||||
require.Equal(t, "max", childChildTwoChildTwoAgg.Aggregation.Type)
|
||||
})
|
||||
|
||||
fields, err := json.Get("fields").StringArray()
|
||||
So(err, ShouldBeNil)
|
||||
So(fields, ShouldHaveLength, 2)
|
||||
So(fields[0], ShouldEqual, "*")
|
||||
So(fields[1], ShouldEqual, "_source")
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
fieldDataFields, err := json.Get("fielddata_fields").StringArray()
|
||||
So(err, ShouldBeNil)
|
||||
So(fieldDataFields, ShouldHaveLength, 1)
|
||||
So(fieldDataFields[0], ShouldEqual, timeField)
|
||||
})
|
||||
termsAgg := json.GetPath("aggs", "1")
|
||||
require.Equal(t, "@hostname", termsAgg.GetPath("terms", "field").MustString())
|
||||
|
||||
termsAggTwo := termsAgg.GetPath("aggs", "2")
|
||||
require.Equal(t, "@app", termsAggTwo.GetPath("terms", "field").MustString())
|
||||
|
||||
termsAggTwoAvg := termsAggTwo.GetPath("aggs", "4")
|
||||
require.Equal(t, "@value", termsAggTwoAvg.GetPath("avg", "field").MustString())
|
||||
|
||||
dateHistAgg := termsAggTwo.GetPath("aggs", "3")
|
||||
require.Equal(t, "@timestamp", dateHistAgg.GetPath("date_histogram", "field").MustString())
|
||||
|
||||
avgAgg := dateHistAgg.GetPath("aggs", "4")
|
||||
require.Equal(t, "@value", avgAgg.GetPath("avg", "field").MustString())
|
||||
|
||||
maxAgg := dateHistAgg.GetPath("aggs", "5")
|
||||
require.Equal(t, "@value", maxAgg.GetPath("max", "field").MustString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Given new search request builder for es version 2", func(t *testing.T) {
|
||||
version2, _ := semver.NewVersion("2.0.0")
|
||||
b := NewSearchRequestBuilder(version2, intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
|
||||
t.Run("When adding doc value field", func(t *testing.T) {
|
||||
b.AddDocValueField(timeField)
|
||||
|
||||
t.Run("should set correct props", func(t *testing.T) {
|
||||
fields, ok := b.customProps["fields"].([]string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 2, len(fields))
|
||||
require.Equal(t, "*", fields[0])
|
||||
require.Equal(t, "_source", fields[1])
|
||||
|
||||
scriptFields, ok := b.customProps["script_fields"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 0, len(scriptFields))
|
||||
|
||||
fieldDataFields, ok := b.customProps["fielddata_fields"].([]string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, len(fieldDataFields))
|
||||
require.Equal(t, timeField, fieldDataFields[0])
|
||||
})
|
||||
|
||||
t.Run("When building search request", func(t *testing.T) {
|
||||
sr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("When marshal to JSON should generate correct json", func(t *testing.T) {
|
||||
body, err := json.Marshal(sr)
|
||||
require.Nil(t, err)
|
||||
json, err := simplejson.NewJson(body)
|
||||
require.Nil(t, err)
|
||||
|
||||
scriptFields, err := json.Get("script_fields").Map()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(scriptFields))
|
||||
|
||||
fields, err := json.Get("fields").StringArray()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 2, len(fields))
|
||||
require.Equal(t, "*", fields[0])
|
||||
require.Equal(t, "_source", fields[1])
|
||||
|
||||
fieldDataFields, err := json.Get("fielddata_fields").StringArray()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(fieldDataFields))
|
||||
require.Equal(t, timeField, fieldDataFields[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -446,31 +454,28 @@ func TestSearchRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMultiSearchRequest(t *testing.T) {
|
||||
Convey("Test elasticsearch multi search request", t, func() {
|
||||
Convey("Given new multi search request builder", func() {
|
||||
version2, _ := semver.NewVersion("2.0.0")
|
||||
b := NewMultiSearchRequestBuilder(version2)
|
||||
t.Run("When adding one search request", func(t *testing.T) {
|
||||
version2, _ := semver.NewVersion("2.0.0")
|
||||
b := NewMultiSearchRequestBuilder(version2)
|
||||
b.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
|
||||
Convey("When adding one search request", func() {
|
||||
b.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
t.Run("When building search request should contain one search request", func(t *testing.T) {
|
||||
mr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(mr.Requests))
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When building search request should contain one search request", func() {
|
||||
mr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
So(mr.Requests, ShouldHaveLength, 1)
|
||||
})
|
||||
})
|
||||
t.Run("When adding two search requests", func(t *testing.T) {
|
||||
version2, _ := semver.NewVersion("2.0.0")
|
||||
b := NewMultiSearchRequestBuilder(version2)
|
||||
b.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
b.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
|
||||
Convey("When adding two search requests", func() {
|
||||
b.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
b.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
|
||||
|
||||
Convey("When building search request should contain two search requests", func() {
|
||||
mr, err := b.Build()
|
||||
So(err, ShouldBeNil)
|
||||
So(mr.Requests, ShouldHaveLength, 2)
|
||||
})
|
||||
})
|
||||
t.Run("When building search request should contain two search requests", func(t *testing.T) {
|
||||
mr, err := b.Build()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 2, len(mr.Requests))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+180
-182
@@ -8,206 +8,204 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMacroEngine(t *testing.T) {
|
||||
Convey("MacroEngine", t, func() {
|
||||
engine := &msSQLMacroEngine{}
|
||||
query := &backend.DataQuery{
|
||||
JSON: []byte("{}"),
|
||||
engine := &msSQLMacroEngine{}
|
||||
query := &backend.DataQuery{
|
||||
JSON: []byte("{}"),
|
||||
}
|
||||
|
||||
dfltTimeRange := backend.TimeRange{}
|
||||
|
||||
t.Run("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func(t *testing.T) {
|
||||
from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
|
||||
to := from.Add(5 * time.Minute)
|
||||
timeRange := backend.TimeRange{From: from, To: to}
|
||||
|
||||
t.Run("interpolate __time function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, dfltTimeRange, "select $__time(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select time_column AS time", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeEpoch function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, dfltTimeRange, "select $__timeEpoch(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select DATEDIFF(second, '1970-01-01', time_column) AS time", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeEpoch function wrapped in aggregation", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, dfltTimeRange, "select min($__timeEpoch(time_column))")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeFrom function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select '2018-04-12T18:00:00Z'", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeTo function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select '2018-04-12T18:05:00Z'", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300", sql)
|
||||
require.Equal(t, sql+" AS [time]", sql2)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function with spaces around arguments", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
|
||||
require.Nil(t, err)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300", sql)
|
||||
require.Equal(t, sql+" AS [time]", sql2)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function with fill (value = NULL)", func(t *testing.T) {
|
||||
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)")
|
||||
require.Nil(t, err)
|
||||
queryJson, err := query.JSON.MarshalJSON()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, `{"fill":true,"fillInterval":300,"fillMode":"null"}`, string(queryJson))
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function with fill (value = previous)", func(t *testing.T) {
|
||||
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', previous)")
|
||||
require.Nil(t, err)
|
||||
queryJson, err := query.JSON.MarshalJSON()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, `{"fill":true,"fillInterval":300,"fillMode":"previous"}`, string(queryJson))
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function with fill (value = float)", func(t *testing.T) {
|
||||
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', 1.5)")
|
||||
require.Nil(t, err)
|
||||
queryJson, err := query.JSON.MarshalJSON()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, `{"fill":true,"fillInterval":300,"fillMode":"value","fillValue":1.5}`, string(queryJson))
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochNanoFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.UnixNano(), to.UnixNano()), sql)
|
||||
})
|
||||
t.Run("interpolate __unixEpochNanoFrom function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFrom()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select %d", from.UnixNano()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochNanoTo function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoTo()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select %d", to.UnixNano()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochGroup function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "SELECT FLOOR(time_column/300)*300", sql)
|
||||
require.Equal(t, sql+" AS [time]", sql2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
|
||||
dfltTimeRange := backend.TimeRange{}
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
|
||||
from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
|
||||
to := from.Add(5 * time.Minute)
|
||||
timeRange := backend.TimeRange{From: from, To: to}
|
||||
|
||||
Convey("interpolate __time function", func() {
|
||||
sql, err := engine.Interpolate(query, dfltTimeRange, "select $__time(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select time_column AS time")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeEpoch function", func() {
|
||||
sql, err := engine.Interpolate(query, dfltTimeRange, "select $__timeEpoch(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeEpoch function wrapped in aggregation", func() {
|
||||
sql, err := engine.Interpolate(query, dfltTimeRange, "select min($__timeEpoch(time_column))")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
|
||||
})
|
||||
|
||||
Convey("interpolate __timeFrom function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select '2018-04-12T18:00:00Z'")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeTo function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select '2018-04-12T18:05:00Z'")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
|
||||
So(sql2, ShouldEqual, sql+" AS [time]")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with spaces around arguments", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
|
||||
So(err, ShouldBeNil)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
|
||||
So(sql2, ShouldEqual, sql+" AS [time]")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
|
||||
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)")
|
||||
So(err, ShouldBeNil)
|
||||
queryJson, err := query.JSON.MarshalJSON()
|
||||
So(err, ShouldBeNil)
|
||||
So(string(queryJson), ShouldEqual, `{"fill":true,"fillInterval":300,"fillMode":"null"}`)
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with fill (value = previous)", func() {
|
||||
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', previous)")
|
||||
So(err, ShouldBeNil)
|
||||
queryJson, err := query.JSON.MarshalJSON()
|
||||
So(err, ShouldBeNil)
|
||||
So(string(queryJson), ShouldEqual, `{"fill":true,"fillInterval":300,"fillMode":"previous"}`)
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with fill (value = float)", func() {
|
||||
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', 1.5)")
|
||||
So(err, ShouldBeNil)
|
||||
queryJson, err := query.JSON.MarshalJSON()
|
||||
So(err, ShouldBeNil)
|
||||
So(string(queryJson), ShouldEqual, `{"fill":true,"fillInterval":300,"fillMode":"value","fillValue":1.5}`)
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.UnixNano(), to.UnixNano()))
|
||||
})
|
||||
Convey("interpolate __unixEpochNanoFrom function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFrom()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select %d", from.UnixNano()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoTo function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoTo()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select %d", to.UnixNano()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochGroup function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "SELECT FLOOR(time_column/300)*300")
|
||||
So(sql2, ShouldEqual, sql+" AS [time]")
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)), sql)
|
||||
})
|
||||
|
||||
Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
t.Run("interpolate __unixEpochFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.UnixNano(), to.UnixNano()))
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
Convey("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func() {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
t.Run("interpolate __unixEpochNanoFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
require.Equal(t, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.UnixNano(), to.UnixNano()), sql)
|
||||
})
|
||||
})
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
|
||||
})
|
||||
t.Run("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()))
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)), sql)
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("interpolate __unixEpochFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.UnixNano(), to.UnixNano()))
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochNanoFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.UnixNano(), to.UnixNano()), sql)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+162
-164
@@ -8,189 +8,187 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMacroEngine(t *testing.T) {
|
||||
Convey("MacroEngine", t, func() {
|
||||
engine := &mySQLMacroEngine{
|
||||
logger: log.New("test"),
|
||||
engine := &mySQLMacroEngine{
|
||||
logger: log.New("test"),
|
||||
}
|
||||
query := &backend.DataQuery{}
|
||||
|
||||
t.Run("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func(t *testing.T) {
|
||||
from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
|
||||
to := from.Add(5 * time.Minute)
|
||||
timeRange := backend.TimeRange{From: from, To: to}
|
||||
|
||||
t.Run("interpolate __time function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select UNIX_TIMESTAMP(time_column) as time_sec", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __time function wrapped in aggregation", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select min($__time(time_column))")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "select min(UNIX_TIMESTAMP(time_column) as time_sec)", sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300", sql)
|
||||
require.Equal(t, sql+" AS \"time\"", sql2)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeGroup function with spaces around arguments", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
|
||||
require.Nil(t, err)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300", sql)
|
||||
require.Equal(t, sql+" AS \"time\"", sql2)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeFrom function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __timeTo function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochNanoFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time)")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select time >= %d AND time <= %d", from.UnixNano(), to.UnixNano()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochNanoFrom function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFrom()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select %d", from.UnixNano()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochNanoTo function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoTo()")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("select %d", to.UnixNano()), sql)
|
||||
})
|
||||
|
||||
t.Run("interpolate __unixEpochGroup function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "SELECT time_column DIV 300 * 300", sql)
|
||||
require.Equal(t, sql+" AS \"time\"", sql2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
query := &backend.DataQuery{}
|
||||
|
||||
Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
|
||||
from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
|
||||
to := from.Add(5 * time.Minute)
|
||||
timeRange := backend.TimeRange{From: from, To: to}
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("interpolate __time function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select UNIX_TIMESTAMP(time_column) as time_sec")
|
||||
})
|
||||
|
||||
Convey("interpolate __time function wrapped in aggregation", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select min($__time(time_column))")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select min(UNIX_TIMESTAMP(time_column) as time_sec)")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
|
||||
So(sql2, ShouldEqual, sql+" AS \"time\"")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with spaces around arguments", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
|
||||
So(err, ShouldBeNil)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
|
||||
So(sql2, ShouldEqual, sql+" AS \"time\"")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __timeFrom function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __timeTo function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFilter(time)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.UnixNano(), to.UnixNano()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoFrom function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoFrom()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select %d", from.UnixNano()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochNanoTo function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochNanoTo()")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select %d", to.UnixNano()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochGroup function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "SELECT time_column DIV 300 * 300")
|
||||
So(sql2, ShouldEqual, sql+" AS \"time\"")
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
t.Run("interpolate __unixEpochFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
require.Equal(t, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
})
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
|
||||
})
|
||||
t.Run("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
|
||||
So(err, ShouldBeNil)
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
require.Nil(t, err)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
|
||||
Convey("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func() {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
t.Run("interpolate __unixEpochFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
|
||||
require.Nil(t, err)
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
|
||||
})
|
||||
require.Equal(t, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()), sql)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given queries that contains unallowed user functions", func() {
|
||||
tcs := []string{
|
||||
"select \nSESSION_USER(), abc",
|
||||
"SELECT session_User( ) ",
|
||||
"SELECT session_User( )\n",
|
||||
"SELECT current_user",
|
||||
"SELECT current_USER",
|
||||
"SELECT current_user()",
|
||||
"SELECT Current_User()",
|
||||
"SELECT current_user( )",
|
||||
"SELECT current_user(\t )",
|
||||
"SELECT user()",
|
||||
"SELECT USER()",
|
||||
"SELECT SYSTEM_USER()",
|
||||
"SELECT System_User()",
|
||||
"SELECT System_User( )",
|
||||
"SELECT System_User(\t \t)",
|
||||
"SHOW \t grants",
|
||||
" show Grants\n",
|
||||
"show grants;",
|
||||
}
|
||||
t.Run("Given queries that contains unallowed user functions", func(t *testing.T) {
|
||||
tcs := []string{
|
||||
"select \nSESSION_USER(), abc",
|
||||
"SELECT session_User( ) ",
|
||||
"SELECT session_User( )\n",
|
||||
"SELECT current_user",
|
||||
"SELECT current_USER",
|
||||
"SELECT current_user()",
|
||||
"SELECT Current_User()",
|
||||
"SELECT current_user( )",
|
||||
"SELECT current_user(\t )",
|
||||
"SELECT user()",
|
||||
"SELECT USER()",
|
||||
"SELECT SYSTEM_USER()",
|
||||
"SELECT System_User()",
|
||||
"SELECT System_User( )",
|
||||
"SELECT System_User(\t \t)",
|
||||
"SHOW \t grants",
|
||||
" show Grants\n",
|
||||
"show grants;",
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
_, err := engine.Interpolate(&backend.DataQuery{}, backend.TimeRange{}, tc)
|
||||
So(err.Error(), ShouldEqual, "invalid query - inspect Grafana server log for details")
|
||||
}
|
||||
})
|
||||
for _, tc := range tcs {
|
||||
_, err := engine.Interpolate(&backend.DataQuery{}, backend.TimeRange{}, tc)
|
||||
require.Equal(t, "invalid query - inspect Grafana server log for details", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,37 +5,19 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/api"
|
||||
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// Internal interval and range variables
|
||||
const (
|
||||
varInterval = "$__interval"
|
||||
varIntervalMs = "$__interval_ms"
|
||||
varRange = "$__range"
|
||||
varRangeS = "$__range_s"
|
||||
varRangeMs = "$__range_ms"
|
||||
varRateInterval = "$__rate_interval"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -116,87 +98,26 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
}
|
||||
}
|
||||
|
||||
//nolint: staticcheck // plugins.DataResponse deprecated
|
||||
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
if len(req.Queries) == 0 {
|
||||
return &backend.QueryDataResponse{}, fmt.Errorf("query contains no queries")
|
||||
}
|
||||
|
||||
q := req.Queries[0]
|
||||
dsInfo, err := s.getDSInfo(req.PluginContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := dsInfo.promClient
|
||||
|
||||
result := backend.QueryDataResponse{
|
||||
Responses: backend.Responses{},
|
||||
var result *backend.QueryDataResponse
|
||||
switch q.QueryType {
|
||||
case "timeSeriesQuery":
|
||||
fallthrough
|
||||
default:
|
||||
result, err = s.executeTimeSeriesQuery(ctx, req, dsInfo)
|
||||
}
|
||||
|
||||
queries, err := s.parseQuery(req, dsInfo)
|
||||
if err != nil {
|
||||
return &result, err
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
plog.Debug("Sending query", "start", query.Start, "end", query.End, "step", query.Step, "query", query.Expr)
|
||||
|
||||
span, ctx := opentracing.StartSpanFromContext(ctx, "datasource.prometheus")
|
||||
span.SetTag("expr", query.Expr)
|
||||
span.SetTag("start_unixnano", query.Start.UnixNano())
|
||||
span.SetTag("stop_unixnano", query.End.UnixNano())
|
||||
defer span.Finish()
|
||||
|
||||
response := make(map[PrometheusQueryType]interface{})
|
||||
|
||||
timeRange := apiv1.Range{
|
||||
Step: query.Step,
|
||||
// Align query range to step. It rounds start and end down to a multiple of step.
|
||||
Start: time.Unix(int64(math.Floor((float64(query.Start.Unix()+query.UtcOffsetSec)/query.Step.Seconds()))*query.Step.Seconds()-float64(query.UtcOffsetSec)), 0),
|
||||
End: time.Unix(int64(math.Floor((float64(query.End.Unix()+query.UtcOffsetSec)/query.Step.Seconds()))*query.Step.Seconds()-float64(query.UtcOffsetSec)), 0),
|
||||
}
|
||||
|
||||
if query.RangeQuery {
|
||||
rangeResponse, _, err := client.QueryRange(ctx, query.Expr, timeRange)
|
||||
if err != nil {
|
||||
plog.Error("Range query", query.Expr, "failed with", err)
|
||||
result.Responses[query.RefId] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response[RangeQueryType] = rangeResponse
|
||||
}
|
||||
}
|
||||
|
||||
if query.InstantQuery {
|
||||
instantResponse, _, err := client.Query(ctx, query.Expr, query.End)
|
||||
if err != nil {
|
||||
plog.Error("Instant query", query.Expr, "failed with", err)
|
||||
result.Responses[query.RefId] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response[InstantQueryType] = instantResponse
|
||||
}
|
||||
}
|
||||
|
||||
if query.ExemplarQuery {
|
||||
exemplarResponse, err := client.QueryExemplars(ctx, query.Expr, timeRange.Start, timeRange.End)
|
||||
if err != nil {
|
||||
plog.Error("Exemplar query", query.Expr, "failed with", err)
|
||||
result.Responses[query.RefId] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response[ExemplarQueryType] = exemplarResponse
|
||||
}
|
||||
}
|
||||
|
||||
frames, err := parseResponse(response, query)
|
||||
if err != nil {
|
||||
return &result, err
|
||||
}
|
||||
|
||||
result.Responses[query.RefId] = backend.DataResponse{
|
||||
Frames: frames,
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
return result, err
|
||||
}
|
||||
|
||||
func createClient(url string, httpOpts sdkhttpclient.Options, clientProvider httpclient.Provider) (apiv1.API, error) {
|
||||
@@ -232,137 +153,6 @@ func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*DatasourceInfo, e
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
func formatLegend(metric model.Metric, query *PrometheusQuery) string {
|
||||
var legend string
|
||||
|
||||
if query.LegendFormat == "" {
|
||||
legend = metric.String()
|
||||
} else {
|
||||
result := legendFormat.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
|
||||
labelName := strings.Replace(string(in), "{{", "", 1)
|
||||
labelName = strings.Replace(labelName, "}}", "", 1)
|
||||
labelName = strings.TrimSpace(labelName)
|
||||
if val, exists := metric[model.LabelName(labelName)]; exists {
|
||||
return []byte(val)
|
||||
}
|
||||
return []byte{}
|
||||
})
|
||||
legend = string(result)
|
||||
}
|
||||
|
||||
// If legend is empty brackets, use query expression
|
||||
if legend == "{}" {
|
||||
legend = query.Expr
|
||||
}
|
||||
|
||||
return legend
|
||||
}
|
||||
|
||||
func (s *Service) parseQuery(queryContext *backend.QueryDataRequest, dsInfo *DatasourceInfo) ([]*PrometheusQuery, error) {
|
||||
qs := []*PrometheusQuery{}
|
||||
for _, query := range queryContext.Queries {
|
||||
model := &QueryModel{}
|
||||
err := json.Unmarshal(query.JSON, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//Final interval value
|
||||
var interval time.Duration
|
||||
|
||||
//Calculate interval
|
||||
queryInterval := model.Interval
|
||||
//If we are using variable or interval/step, we will replace it with calculated interval
|
||||
if queryInterval == varInterval || queryInterval == varIntervalMs || queryInterval == varRateInterval {
|
||||
queryInterval = ""
|
||||
}
|
||||
minInterval, err := intervalv2.GetIntervalFrom(dsInfo.TimeInterval, queryInterval, model.IntervalMS, 15*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
calculatedInterval := s.intervalCalculator.Calculate(query.TimeRange, minInterval, query.MaxDataPoints)
|
||||
safeInterval := s.intervalCalculator.CalculateSafeInterval(query.TimeRange, int64(safeRes))
|
||||
adjustedInterval := safeInterval.Value
|
||||
|
||||
if calculatedInterval.Value > safeInterval.Value {
|
||||
adjustedInterval = calculatedInterval.Value
|
||||
}
|
||||
|
||||
if queryInterval == varRateInterval {
|
||||
// Rate interval is final and is not affected by resolution
|
||||
interval = calculateRateInterval(adjustedInterval, dsInfo.TimeInterval, s.intervalCalculator)
|
||||
} else {
|
||||
intervalFactor := model.IntervalFactor
|
||||
if intervalFactor == 0 {
|
||||
intervalFactor = 1
|
||||
}
|
||||
interval = time.Duration(int64(adjustedInterval) * intervalFactor)
|
||||
}
|
||||
|
||||
intervalMs := int64(interval / time.Millisecond)
|
||||
rangeS := query.TimeRange.To.Unix() - query.TimeRange.From.Unix()
|
||||
|
||||
// Interpolate variables in expr
|
||||
expr := model.Expr
|
||||
expr = strings.ReplaceAll(expr, varIntervalMs, strconv.FormatInt(intervalMs, 10))
|
||||
expr = strings.ReplaceAll(expr, varInterval, intervalv2.FormatDuration(interval))
|
||||
expr = strings.ReplaceAll(expr, varRangeMs, strconv.FormatInt(rangeS*1000, 10))
|
||||
expr = strings.ReplaceAll(expr, varRangeS, strconv.FormatInt(rangeS, 10))
|
||||
expr = strings.ReplaceAll(expr, varRange, strconv.FormatInt(rangeS, 10)+"s")
|
||||
expr = strings.ReplaceAll(expr, varRateInterval, intervalv2.FormatDuration(calculateRateInterval(interval, dsInfo.TimeInterval, s.intervalCalculator)))
|
||||
|
||||
rangeQuery := model.RangeQuery
|
||||
if !model.InstantQuery && !model.RangeQuery {
|
||||
// In older dashboards, we were not setting range query param and !range && !instant was run as range query
|
||||
rangeQuery = true
|
||||
}
|
||||
|
||||
qs = append(qs, &PrometheusQuery{
|
||||
Expr: expr,
|
||||
Step: interval,
|
||||
LegendFormat: model.LegendFormat,
|
||||
Start: query.TimeRange.From,
|
||||
End: query.TimeRange.To,
|
||||
RefId: query.RefID,
|
||||
InstantQuery: model.InstantQuery,
|
||||
RangeQuery: rangeQuery,
|
||||
ExemplarQuery: model.ExemplarQuery,
|
||||
UtcOffsetSec: model.UtcOffsetSec,
|
||||
})
|
||||
}
|
||||
return qs, nil
|
||||
}
|
||||
|
||||
func parseResponse(value map[PrometheusQueryType]interface{}, query *PrometheusQuery) (data.Frames, error) {
|
||||
var (
|
||||
frames = data.Frames{}
|
||||
nextFrames = data.Frames{}
|
||||
)
|
||||
|
||||
for _, value := range value {
|
||||
// Zero out the slice to prevent data corruption.
|
||||
nextFrames = nextFrames[:0]
|
||||
|
||||
switch v := value.(type) {
|
||||
case model.Matrix:
|
||||
nextFrames = matrixToDataFrames(v, query, nextFrames)
|
||||
case model.Vector:
|
||||
nextFrames = vectorToDataFrames(v, query, nextFrames)
|
||||
case *model.Scalar:
|
||||
nextFrames = scalarToDataFrames(v, query, nextFrames)
|
||||
case []apiv1.ExemplarQueryResult:
|
||||
nextFrames = exemplarToDataFrames(v, query, nextFrames)
|
||||
default:
|
||||
plog.Error("Query", query.Expr, "returned unexpected result type", v)
|
||||
continue
|
||||
}
|
||||
|
||||
frames = append(frames, nextFrames...)
|
||||
}
|
||||
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
// IsAPIError returns whether err is or wraps a Prometheus error.
|
||||
func IsAPIError(err error) bool {
|
||||
// Check if the right error type is in err's chain.
|
||||
@@ -377,230 +167,3 @@ func ConvertAPIError(err error) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func calculateRateInterval(interval time.Duration, scrapeInterval string, intervalCalculator intervalv2.Calculator) time.Duration {
|
||||
scrape := scrapeInterval
|
||||
if scrape == "" {
|
||||
scrape = "15s"
|
||||
}
|
||||
|
||||
scrapeIntervalDuration, err := intervalv2.ParseIntervalStringToTimeDuration(scrape)
|
||||
if err != nil {
|
||||
return time.Duration(0)
|
||||
}
|
||||
|
||||
rateInterval := time.Duration(int(math.Max(float64(interval+scrapeIntervalDuration), float64(4)*float64(scrapeIntervalDuration))))
|
||||
return rateInterval
|
||||
}
|
||||
|
||||
func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
for _, v := range matrix {
|
||||
tags := make(map[string]string, len(v.Metric))
|
||||
for k, v := range v.Metric {
|
||||
tags[string(k)] = string(v)
|
||||
}
|
||||
|
||||
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, len(v.Values))
|
||||
valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(v.Values))
|
||||
|
||||
for i, k := range v.Values {
|
||||
timeField.Set(i, time.Unix(k.Timestamp.Unix(), 0).UTC())
|
||||
value := float64(k.Value)
|
||||
if !math.IsNaN(value) {
|
||||
valueField.Set(i, &value)
|
||||
}
|
||||
}
|
||||
|
||||
name := formatLegend(v.Metric, query)
|
||||
timeField.Name = data.TimeSeriesTimeFieldName
|
||||
valueField.Name = data.TimeSeriesValueFieldName
|
||||
valueField.Config = &data.FieldConfig{DisplayNameFromDS: name}
|
||||
valueField.Labels = tags
|
||||
|
||||
frames = append(frames, newDataFrame(name, "matrix", timeField, valueField))
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func scalarToDataFrames(scalar *model.Scalar, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
timeVector := []time.Time{time.Unix(scalar.Timestamp.Unix(), 0).UTC()}
|
||||
values := []float64{float64(scalar.Value)}
|
||||
name := fmt.Sprintf("%g", values[0])
|
||||
|
||||
return append(
|
||||
frames,
|
||||
newDataFrame(
|
||||
name,
|
||||
"scalar",
|
||||
data.NewField("Time", nil, timeVector),
|
||||
data.NewField("Value", nil, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func vectorToDataFrames(vector model.Vector, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
for _, v := range vector {
|
||||
name := formatLegend(v.Metric, query)
|
||||
tags := make(map[string]string, len(v.Metric))
|
||||
timeVector := []time.Time{time.Unix(v.Timestamp.Unix(), 0).UTC()}
|
||||
values := []float64{float64(v.Value)}
|
||||
|
||||
for k, v := range v.Metric {
|
||||
tags[string(k)] = string(v)
|
||||
}
|
||||
|
||||
frames = append(
|
||||
frames,
|
||||
newDataFrame(
|
||||
name,
|
||||
"vector",
|
||||
data.NewField("Time", nil, timeVector),
|
||||
data.NewField("Value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func exemplarToDataFrames(response []apiv1.ExemplarQueryResult, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
// TODO: this preallocation is very naive.
|
||||
// We should figure out a better approximation here.
|
||||
events := make([]ExemplarEvent, 0, len(response)*2)
|
||||
|
||||
for _, exemplarData := range response {
|
||||
for _, exemplar := range exemplarData.Exemplars {
|
||||
event := ExemplarEvent{}
|
||||
exemplarTime := time.Unix(exemplar.Timestamp.Unix(), 0).UTC()
|
||||
event.Time = exemplarTime
|
||||
event.Value = float64(exemplar.Value)
|
||||
event.Labels = make(map[string]string)
|
||||
|
||||
for label, value := range exemplar.Labels {
|
||||
event.Labels[string(label)] = string(value)
|
||||
}
|
||||
|
||||
for seriesLabel, seriesValue := range exemplarData.SeriesLabels {
|
||||
event.Labels[string(seriesLabel)] = string(seriesValue)
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
|
||||
// Sampling of exemplars
|
||||
bucketedExemplars := make(map[string][]ExemplarEvent)
|
||||
values := make([]float64, 0, len(events))
|
||||
|
||||
// Create bucketed exemplars based on aligned timestamp
|
||||
for _, event := range events {
|
||||
alignedTs := fmt.Sprintf("%.0f", math.Floor(float64(event.Time.Unix())/query.Step.Seconds())*query.Step.Seconds())
|
||||
_, ok := bucketedExemplars[alignedTs]
|
||||
if !ok {
|
||||
bucketedExemplars[alignedTs] = make([]ExemplarEvent, 0)
|
||||
}
|
||||
|
||||
bucketedExemplars[alignedTs] = append(bucketedExemplars[alignedTs], event)
|
||||
values = append(values, event.Value)
|
||||
}
|
||||
|
||||
// Calculate standard deviation
|
||||
standardDeviation := deviation(values)
|
||||
|
||||
// Create slice with all of the bucketed exemplars
|
||||
sampledBuckets := make([]string, len(bucketedExemplars))
|
||||
for bucketTimes := range bucketedExemplars {
|
||||
sampledBuckets = append(sampledBuckets, bucketTimes)
|
||||
}
|
||||
sort.Strings(sampledBuckets)
|
||||
|
||||
// Sample exemplars based ona value, so we are not showing too many of them
|
||||
sampleExemplars := make([]ExemplarEvent, 0, len(sampledBuckets))
|
||||
for _, bucket := range sampledBuckets {
|
||||
exemplarsInBucket := bucketedExemplars[bucket]
|
||||
if len(exemplarsInBucket) == 1 {
|
||||
sampleExemplars = append(sampleExemplars, exemplarsInBucket[0])
|
||||
} else {
|
||||
bucketValues := make([]float64, len(exemplarsInBucket))
|
||||
for _, exemplar := range exemplarsInBucket {
|
||||
bucketValues = append(bucketValues, exemplar.Value)
|
||||
}
|
||||
sort.Slice(bucketValues, func(i, j int) bool {
|
||||
return bucketValues[i] > bucketValues[j]
|
||||
})
|
||||
|
||||
sampledBucketValues := make([]float64, 0)
|
||||
for _, value := range bucketValues {
|
||||
if len(sampledBucketValues) == 0 {
|
||||
sampledBucketValues = append(sampledBucketValues, value)
|
||||
} else {
|
||||
// Then take values only when at least 2 standard deviation distance to previously taken value
|
||||
prev := sampledBucketValues[len(sampledBucketValues)-1]
|
||||
if standardDeviation != 0 && prev-value >= float64(2)*standardDeviation {
|
||||
sampledBucketValues = append(sampledBucketValues, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, valueBucket := range sampledBucketValues {
|
||||
for _, exemplar := range exemplarsInBucket {
|
||||
if exemplar.Value == valueBucket {
|
||||
sampleExemplars = append(sampleExemplars, exemplar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create DF from sampled exemplars
|
||||
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, len(sampleExemplars))
|
||||
timeField.Name = "Time"
|
||||
valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, len(sampleExemplars))
|
||||
valueField.Name = "Value"
|
||||
labelsVector := make(map[string][]string, len(sampleExemplars))
|
||||
|
||||
for i, exemplar := range sampleExemplars {
|
||||
timeField.Set(i, exemplar.Time)
|
||||
valueField.Set(i, exemplar.Value)
|
||||
|
||||
for label, value := range exemplar.Labels {
|
||||
if labelsVector[label] == nil {
|
||||
labelsVector[label] = make([]string, 0)
|
||||
}
|
||||
|
||||
labelsVector[label] = append(labelsVector[label], value)
|
||||
}
|
||||
}
|
||||
|
||||
dataFields := make([]*data.Field, 0, len(labelsVector)+2)
|
||||
dataFields = append(dataFields, timeField, valueField)
|
||||
for label, vector := range labelsVector {
|
||||
dataFields = append(dataFields, data.NewField(label, nil, vector))
|
||||
}
|
||||
|
||||
return append(frames, newDataFrame("exemplar", "exemplar", dataFields...))
|
||||
}
|
||||
|
||||
func deviation(values []float64) float64 {
|
||||
var sum, mean, sd float64
|
||||
valuesLen := float64(len(values))
|
||||
for _, value := range values {
|
||||
sum += value
|
||||
}
|
||||
mean = sum / valuesLen
|
||||
for j := 0; j < len(values); j++ {
|
||||
sd += math.Pow(values[j]-mean, 2)
|
||||
}
|
||||
return math.Sqrt(sd / (valuesLen - 1))
|
||||
}
|
||||
|
||||
func newDataFrame(name string, typ string, fields ...*data.Field) *data.Frame {
|
||||
frame := data.NewFrame(name, fields...)
|
||||
frame.Meta = &data.FrameMeta{
|
||||
Custom: map[string]string{
|
||||
"resultType": typ,
|
||||
},
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// Internal interval and range variables
|
||||
const (
|
||||
varInterval = "$__interval"
|
||||
varIntervalMs = "$__interval_ms"
|
||||
varRange = "$__range"
|
||||
varRangeS = "$__range_s"
|
||||
varRangeMs = "$__range_ms"
|
||||
varRateInterval = "$__rate_interval"
|
||||
)
|
||||
|
||||
type TimeSeriesQueryType string
|
||||
|
||||
const (
|
||||
RangeQueryType TimeSeriesQueryType = "range"
|
||||
InstantQueryType TimeSeriesQueryType = "instant"
|
||||
ExemplarQueryType TimeSeriesQueryType = "exemplar"
|
||||
)
|
||||
|
||||
func (s *Service) executeTimeSeriesQuery(ctx context.Context, req *backend.QueryDataRequest, dsInfo *DatasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
client := dsInfo.promClient
|
||||
|
||||
result := backend.QueryDataResponse{
|
||||
Responses: backend.Responses{},
|
||||
}
|
||||
|
||||
queries, err := s.parseTimeSeriesQuery(req, dsInfo)
|
||||
if err != nil {
|
||||
return &result, err
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
plog.Debug("Sending query", "start", query.Start, "end", query.End, "step", query.Step, "query", query.Expr)
|
||||
|
||||
span, ctx := opentracing.StartSpanFromContext(ctx, "datasource.prometheus")
|
||||
span.SetTag("expr", query.Expr)
|
||||
span.SetTag("start_unixnano", query.Start.UnixNano())
|
||||
span.SetTag("stop_unixnano", query.End.UnixNano())
|
||||
defer span.Finish()
|
||||
|
||||
response := make(map[TimeSeriesQueryType]interface{})
|
||||
|
||||
timeRange := apiv1.Range{
|
||||
Step: query.Step,
|
||||
// Align query range to step. It rounds start and end down to a multiple of step.
|
||||
Start: time.Unix(int64(math.Floor((float64(query.Start.Unix()+query.UtcOffsetSec)/query.Step.Seconds()))*query.Step.Seconds()-float64(query.UtcOffsetSec)), 0),
|
||||
End: time.Unix(int64(math.Floor((float64(query.End.Unix()+query.UtcOffsetSec)/query.Step.Seconds()))*query.Step.Seconds()-float64(query.UtcOffsetSec)), 0),
|
||||
}
|
||||
|
||||
if query.RangeQuery {
|
||||
rangeResponse, _, err := client.QueryRange(ctx, query.Expr, timeRange)
|
||||
if err != nil {
|
||||
plog.Error("Range query", query.Expr, "failed with", err)
|
||||
result.Responses[query.RefId] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response[RangeQueryType] = rangeResponse
|
||||
}
|
||||
}
|
||||
|
||||
if query.InstantQuery {
|
||||
instantResponse, _, err := client.Query(ctx, query.Expr, query.End)
|
||||
if err != nil {
|
||||
plog.Error("Instant query", query.Expr, "failed with", err)
|
||||
result.Responses[query.RefId] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response[InstantQueryType] = instantResponse
|
||||
}
|
||||
}
|
||||
|
||||
if query.ExemplarQuery {
|
||||
exemplarResponse, err := client.QueryExemplars(ctx, query.Expr, timeRange.Start, timeRange.End)
|
||||
if err != nil {
|
||||
plog.Error("Exemplar query", query.Expr, "failed with", err)
|
||||
result.Responses[query.RefId] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response[ExemplarQueryType] = exemplarResponse
|
||||
}
|
||||
}
|
||||
|
||||
frames, err := parseTimeSeriesResponse(response, query)
|
||||
if err != nil {
|
||||
return &result, err
|
||||
}
|
||||
|
||||
result.Responses[query.RefId] = backend.DataResponse{
|
||||
Frames: frames,
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func formatLegend(metric model.Metric, query *PrometheusQuery) string {
|
||||
var legend string
|
||||
|
||||
if query.LegendFormat == "" {
|
||||
legend = metric.String()
|
||||
} else {
|
||||
result := legendFormat.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
|
||||
labelName := strings.Replace(string(in), "{{", "", 1)
|
||||
labelName = strings.Replace(labelName, "}}", "", 1)
|
||||
labelName = strings.TrimSpace(labelName)
|
||||
if val, exists := metric[model.LabelName(labelName)]; exists {
|
||||
return []byte(val)
|
||||
}
|
||||
return []byte{}
|
||||
})
|
||||
legend = string(result)
|
||||
}
|
||||
|
||||
// If legend is empty brackets, use query expression
|
||||
if legend == "{}" {
|
||||
legend = query.Expr
|
||||
}
|
||||
|
||||
return legend
|
||||
}
|
||||
|
||||
func (s *Service) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest, dsInfo *DatasourceInfo) ([]*PrometheusQuery, error) {
|
||||
qs := []*PrometheusQuery{}
|
||||
for _, query := range queryContext.Queries {
|
||||
model := &QueryModel{}
|
||||
err := json.Unmarshal(query.JSON, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//Final interval value
|
||||
var interval time.Duration
|
||||
|
||||
//Calculate interval
|
||||
queryInterval := model.Interval
|
||||
//If we are using variable or interval/step, we will replace it with calculated interval
|
||||
if queryInterval == varInterval || queryInterval == varIntervalMs || queryInterval == varRateInterval {
|
||||
queryInterval = ""
|
||||
}
|
||||
minInterval, err := intervalv2.GetIntervalFrom(dsInfo.TimeInterval, queryInterval, model.IntervalMS, 15*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
calculatedInterval := s.intervalCalculator.Calculate(query.TimeRange, minInterval, query.MaxDataPoints)
|
||||
safeInterval := s.intervalCalculator.CalculateSafeInterval(query.TimeRange, int64(safeRes))
|
||||
adjustedInterval := safeInterval.Value
|
||||
|
||||
if calculatedInterval.Value > safeInterval.Value {
|
||||
adjustedInterval = calculatedInterval.Value
|
||||
}
|
||||
|
||||
if queryInterval == varRateInterval {
|
||||
// Rate interval is final and is not affected by resolution
|
||||
interval = calculateRateInterval(adjustedInterval, dsInfo.TimeInterval, s.intervalCalculator)
|
||||
} else {
|
||||
intervalFactor := model.IntervalFactor
|
||||
if intervalFactor == 0 {
|
||||
intervalFactor = 1
|
||||
}
|
||||
interval = time.Duration(int64(adjustedInterval) * intervalFactor)
|
||||
}
|
||||
|
||||
intervalMs := int64(interval / time.Millisecond)
|
||||
rangeS := query.TimeRange.To.Unix() - query.TimeRange.From.Unix()
|
||||
|
||||
// Interpolate variables in expr
|
||||
expr := model.Expr
|
||||
expr = strings.ReplaceAll(expr, varIntervalMs, strconv.FormatInt(intervalMs, 10))
|
||||
expr = strings.ReplaceAll(expr, varInterval, intervalv2.FormatDuration(interval))
|
||||
expr = strings.ReplaceAll(expr, varRangeMs, strconv.FormatInt(rangeS*1000, 10))
|
||||
expr = strings.ReplaceAll(expr, varRangeS, strconv.FormatInt(rangeS, 10))
|
||||
expr = strings.ReplaceAll(expr, varRange, strconv.FormatInt(rangeS, 10)+"s")
|
||||
expr = strings.ReplaceAll(expr, varRateInterval, intervalv2.FormatDuration(calculateRateInterval(interval, dsInfo.TimeInterval, s.intervalCalculator)))
|
||||
|
||||
rangeQuery := model.RangeQuery
|
||||
if !model.InstantQuery && !model.RangeQuery {
|
||||
// In older dashboards, we were not setting range query param and !range && !instant was run as range query
|
||||
rangeQuery = true
|
||||
}
|
||||
|
||||
qs = append(qs, &PrometheusQuery{
|
||||
Expr: expr,
|
||||
Step: interval,
|
||||
LegendFormat: model.LegendFormat,
|
||||
Start: query.TimeRange.From,
|
||||
End: query.TimeRange.To,
|
||||
RefId: query.RefID,
|
||||
InstantQuery: model.InstantQuery,
|
||||
RangeQuery: rangeQuery,
|
||||
ExemplarQuery: model.ExemplarQuery,
|
||||
UtcOffsetSec: model.UtcOffsetSec,
|
||||
})
|
||||
}
|
||||
return qs, nil
|
||||
}
|
||||
|
||||
func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery) (data.Frames, error) {
|
||||
var (
|
||||
frames = data.Frames{}
|
||||
nextFrames = data.Frames{}
|
||||
)
|
||||
|
||||
for _, value := range value {
|
||||
// Zero out the slice to prevent data corruption.
|
||||
nextFrames = nextFrames[:0]
|
||||
|
||||
switch v := value.(type) {
|
||||
case model.Matrix:
|
||||
nextFrames = matrixToDataFrames(v, query, nextFrames)
|
||||
case model.Vector:
|
||||
nextFrames = vectorToDataFrames(v, query, nextFrames)
|
||||
case *model.Scalar:
|
||||
nextFrames = scalarToDataFrames(v, query, nextFrames)
|
||||
case []apiv1.ExemplarQueryResult:
|
||||
nextFrames = exemplarToDataFrames(v, query, nextFrames)
|
||||
default:
|
||||
plog.Error("Query", query.Expr, "returned unexpected result type", v)
|
||||
continue
|
||||
}
|
||||
|
||||
frames = append(frames, nextFrames...)
|
||||
}
|
||||
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
func calculateRateInterval(interval time.Duration, scrapeInterval string, intervalCalculator intervalv2.Calculator) time.Duration {
|
||||
scrape := scrapeInterval
|
||||
if scrape == "" {
|
||||
scrape = "15s"
|
||||
}
|
||||
|
||||
scrapeIntervalDuration, err := intervalv2.ParseIntervalStringToTimeDuration(scrape)
|
||||
if err != nil {
|
||||
return time.Duration(0)
|
||||
}
|
||||
|
||||
rateInterval := time.Duration(int(math.Max(float64(interval+scrapeIntervalDuration), float64(4)*float64(scrapeIntervalDuration))))
|
||||
return rateInterval
|
||||
}
|
||||
|
||||
func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
for _, v := range matrix {
|
||||
tags := make(map[string]string, len(v.Metric))
|
||||
for k, v := range v.Metric {
|
||||
tags[string(k)] = string(v)
|
||||
}
|
||||
|
||||
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, len(v.Values))
|
||||
valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(v.Values))
|
||||
|
||||
for i, k := range v.Values {
|
||||
timeField.Set(i, time.Unix(k.Timestamp.Unix(), 0).UTC())
|
||||
value := float64(k.Value)
|
||||
if !math.IsNaN(value) {
|
||||
valueField.Set(i, &value)
|
||||
}
|
||||
}
|
||||
|
||||
name := formatLegend(v.Metric, query)
|
||||
timeField.Name = data.TimeSeriesTimeFieldName
|
||||
valueField.Name = data.TimeSeriesValueFieldName
|
||||
valueField.Config = &data.FieldConfig{DisplayNameFromDS: name}
|
||||
valueField.Labels = tags
|
||||
|
||||
frames = append(frames, newDataFrame(name, "matrix", timeField, valueField))
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func scalarToDataFrames(scalar *model.Scalar, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
timeVector := []time.Time{time.Unix(scalar.Timestamp.Unix(), 0).UTC()}
|
||||
values := []float64{float64(scalar.Value)}
|
||||
name := fmt.Sprintf("%g", values[0])
|
||||
|
||||
return append(
|
||||
frames,
|
||||
newDataFrame(
|
||||
name,
|
||||
"scalar",
|
||||
data.NewField("Time", nil, timeVector),
|
||||
data.NewField("Value", nil, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func vectorToDataFrames(vector model.Vector, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
for _, v := range vector {
|
||||
name := formatLegend(v.Metric, query)
|
||||
tags := make(map[string]string, len(v.Metric))
|
||||
timeVector := []time.Time{time.Unix(v.Timestamp.Unix(), 0).UTC()}
|
||||
values := []float64{float64(v.Value)}
|
||||
|
||||
for k, v := range v.Metric {
|
||||
tags[string(k)] = string(v)
|
||||
}
|
||||
|
||||
frames = append(
|
||||
frames,
|
||||
newDataFrame(
|
||||
name,
|
||||
"vector",
|
||||
data.NewField("Time", nil, timeVector),
|
||||
data.NewField("Value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func exemplarToDataFrames(response []apiv1.ExemplarQueryResult, query *PrometheusQuery, frames data.Frames) data.Frames {
|
||||
// TODO: this preallocation is very naive.
|
||||
// We should figure out a better approximation here.
|
||||
events := make([]ExemplarEvent, 0, len(response)*2)
|
||||
|
||||
for _, exemplarData := range response {
|
||||
for _, exemplar := range exemplarData.Exemplars {
|
||||
event := ExemplarEvent{}
|
||||
exemplarTime := time.Unix(exemplar.Timestamp.Unix(), 0).UTC()
|
||||
event.Time = exemplarTime
|
||||
event.Value = float64(exemplar.Value)
|
||||
event.Labels = make(map[string]string)
|
||||
|
||||
for label, value := range exemplar.Labels {
|
||||
event.Labels[string(label)] = string(value)
|
||||
}
|
||||
|
||||
for seriesLabel, seriesValue := range exemplarData.SeriesLabels {
|
||||
event.Labels[string(seriesLabel)] = string(seriesValue)
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
|
||||
// Sampling of exemplars
|
||||
bucketedExemplars := make(map[string][]ExemplarEvent)
|
||||
values := make([]float64, 0, len(events))
|
||||
|
||||
// Create bucketed exemplars based on aligned timestamp
|
||||
for _, event := range events {
|
||||
alignedTs := fmt.Sprintf("%.0f", math.Floor(float64(event.Time.Unix())/query.Step.Seconds())*query.Step.Seconds())
|
||||
_, ok := bucketedExemplars[alignedTs]
|
||||
if !ok {
|
||||
bucketedExemplars[alignedTs] = make([]ExemplarEvent, 0)
|
||||
}
|
||||
|
||||
bucketedExemplars[alignedTs] = append(bucketedExemplars[alignedTs], event)
|
||||
values = append(values, event.Value)
|
||||
}
|
||||
|
||||
// Calculate standard deviation
|
||||
standardDeviation := deviation(values)
|
||||
|
||||
// Create slice with all of the bucketed exemplars
|
||||
sampledBuckets := make([]string, len(bucketedExemplars))
|
||||
for bucketTimes := range bucketedExemplars {
|
||||
sampledBuckets = append(sampledBuckets, bucketTimes)
|
||||
}
|
||||
sort.Strings(sampledBuckets)
|
||||
|
||||
// Sample exemplars based ona value, so we are not showing too many of them
|
||||
sampleExemplars := make([]ExemplarEvent, 0, len(sampledBuckets))
|
||||
for _, bucket := range sampledBuckets {
|
||||
exemplarsInBucket := bucketedExemplars[bucket]
|
||||
if len(exemplarsInBucket) == 1 {
|
||||
sampleExemplars = append(sampleExemplars, exemplarsInBucket[0])
|
||||
} else {
|
||||
bucketValues := make([]float64, len(exemplarsInBucket))
|
||||
for _, exemplar := range exemplarsInBucket {
|
||||
bucketValues = append(bucketValues, exemplar.Value)
|
||||
}
|
||||
sort.Slice(bucketValues, func(i, j int) bool {
|
||||
return bucketValues[i] > bucketValues[j]
|
||||
})
|
||||
|
||||
sampledBucketValues := make([]float64, 0)
|
||||
for _, value := range bucketValues {
|
||||
if len(sampledBucketValues) == 0 {
|
||||
sampledBucketValues = append(sampledBucketValues, value)
|
||||
} else {
|
||||
// Then take values only when at least 2 standard deviation distance to previously taken value
|
||||
prev := sampledBucketValues[len(sampledBucketValues)-1]
|
||||
if standardDeviation != 0 && prev-value >= float64(2)*standardDeviation {
|
||||
sampledBucketValues = append(sampledBucketValues, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, valueBucket := range sampledBucketValues {
|
||||
for _, exemplar := range exemplarsInBucket {
|
||||
if exemplar.Value == valueBucket {
|
||||
sampleExemplars = append(sampleExemplars, exemplar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create DF from sampled exemplars
|
||||
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, len(sampleExemplars))
|
||||
timeField.Name = "Time"
|
||||
valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, len(sampleExemplars))
|
||||
valueField.Name = "Value"
|
||||
labelsVector := make(map[string][]string, len(sampleExemplars))
|
||||
|
||||
for i, exemplar := range sampleExemplars {
|
||||
timeField.Set(i, exemplar.Time)
|
||||
valueField.Set(i, exemplar.Value)
|
||||
|
||||
for label, value := range exemplar.Labels {
|
||||
if labelsVector[label] == nil {
|
||||
labelsVector[label] = make([]string, 0)
|
||||
}
|
||||
|
||||
labelsVector[label] = append(labelsVector[label], value)
|
||||
}
|
||||
}
|
||||
|
||||
dataFields := make([]*data.Field, 0, len(labelsVector)+2)
|
||||
dataFields = append(dataFields, timeField, valueField)
|
||||
for label, vector := range labelsVector {
|
||||
dataFields = append(dataFields, data.NewField(label, nil, vector))
|
||||
}
|
||||
|
||||
return append(frames, newDataFrame("exemplar", "exemplar", dataFields...))
|
||||
}
|
||||
|
||||
func deviation(values []float64) float64 {
|
||||
var sum, mean, sd float64
|
||||
valuesLen := float64(len(values))
|
||||
for _, value := range values {
|
||||
sum += value
|
||||
}
|
||||
mean = sum / valuesLen
|
||||
for j := 0; j < len(values); j++ {
|
||||
sd += math.Pow(values[j]-mean, 2)
|
||||
}
|
||||
return math.Sqrt(sd / (valuesLen - 1))
|
||||
}
|
||||
|
||||
func newDataFrame(name string, typ string, fields ...*data.Field) *data.Frame {
|
||||
frame := data.NewFrame(name, fields...)
|
||||
frame.Meta = &data.FrameMeta{
|
||||
Custom: map[string]string{
|
||||
"resultType": typ,
|
||||
},
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user