Merge branch 'main' into encryption/use-secrets-service

This commit is contained in:
Tania B
2021-10-27 11:57:06 +03:00
committed by GitHub
207 changed files with 6836 additions and 3723 deletions
+7
View File
@@ -15,4 +15,11 @@ node_modules
/local
/tmp
*.yml
!.yarnrc.yml
*.md
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/versions
!.yarn/cache
+1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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" >}}).
+6 -5
View File
@@ -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
+1 -1
View File
@@ -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>
+14
View File
@@ -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.
![Change legend series color](/static/img/docs/legend/legend-series-color-7-5.png)
## 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.
![Sort legend series](/static/img/docs/legend/legend-series-sort-8-3.png)
+1 -1
View File
@@ -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');
},
});
+1 -1
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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;
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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>;
+138 -2
View File
@@ -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")
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
}
}
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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))
}
+1 -1
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -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 + "/")
+1 -1
View File
@@ -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
+3
View File
@@ -1,3 +1,6 @@
//go:build integration
// +build integration
package kvstore
import (
+4 -2
View File
@@ -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)
}
+8
View File
@@ -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...)
}
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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)
})
}
+137 -139
View File
@@ -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
+314 -317
View File
@@ -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{}
})
})
}
+87 -88
View File
@@ -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)
})
})
}
+154 -156
View File
@@ -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)
})
}
+21 -10
View File
@@ -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)
+1 -1
View File
@@ -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),
+93 -16
View File
@@ -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()
+12 -11
View File
@@ -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)
})
})
}
+1 -1
View File
@@ -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)
}
+5
View File
@@ -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 {
+1 -1
View File
@@ -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)
}
}
}
+15 -14
View File
@@ -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)
})
})
})
+9 -6
View File
@@ -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)
}
}
+15 -16
View File
@@ -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
View File
@@ -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&timespan=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&timespan=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&timespan=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&timespan=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&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
require.Equal(t, queries[0].Target, "aggregation=Average&interval=PT1M&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
})
})
})
+12 -12
View File
@@ -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")
})
})
}
+33 -60
View File
@@ -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
}
+56
View File
@@ -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
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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())
}
})
}
+8 -445
View File
@@ -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
}
+468
View File
@@ -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