diff --git a/CHANGELOG.md b/CHANGELOG.md index 5483529f75e..b648603579a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow) * **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683) * **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683) +* **VictorOps**: Adds more information to the victor ops notifiers [#15744](https://github.com/grafana/grafana/issues/15744), thx [@zhulongcheng](https://github.com/zhulongcheng) ### Bug Fixes * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index ed88fcfc509..9c7fd5207c3 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -105,6 +105,14 @@ region = us-west-2 You need to specify a namespace, metric, at least one stat, and at least one dimension. +## Metric Math + +You can now create new time series metrics by operating on top of Cloudwatch metrics using mathematical functions. Arithmetic operators, unary subtraction and other functions are supported to be applied on cloudwatch metrics. More details on the available functions can be found on [AWS Metric Math](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) + +As an example, if you want to apply arithmetic operator on a metric, you can do it by giving an alias(a unique string) to the raw metric as shown below. Then you can use this alias and apply arithmetic operator to it in the Expression field of created metric. + +![](/img/docs/v60/cloudwatch_metric_math.png) + ## Templated queries Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. diff --git a/docs/sources/plugins/developing/datasources.md b/docs/sources/plugins/developing/datasources.md index f8792441bbd..7be1b754865 100644 --- a/docs/sources/plugins/developing/datasources.md +++ b/docs/sources/plugins/developing/datasources.md @@ -163,7 +163,7 @@ Expected result from datasource.annotationQuery: "title": "Cluster outage", "time": 1457075272576, "text": "Joe causes brain split", - "tags": "joe, cluster, failure" + "tags": ["joe", "cluster", "failure"] } ] ``` diff --git a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts index 97ade6da7a5..16f4a20d139 100644 --- a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts @@ -120,7 +120,7 @@ $headings-line-height: ${theme.typography.lineHeight.sm} !default; $border-width: ${theme.border.width.sm} !default; $border-radius: ${theme.border.radius.md} !default; -$border-radius-lg: ${theme.border.radius.lg}!default; +$border-radius-lg: ${theme.border.radius.lg} !default; $border-radius-sm: ${theme.border.radius.sm} !default; // Page @@ -191,7 +191,6 @@ $btn-padding-y-lg: 11px !default; $btn-padding-x-xl: 21px !default; $btn-padding-y-xl: 11px !default; - $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default; // sidemenu diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 889e4ab310e..2307f446a98 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -26,13 +26,21 @@ export interface PanelEditorProps { onOptionsChange: (options: T) => void; } -export type PreservePanelOptionsHandler = (pluginId: string, prevOptions: any) => Partial; +/** + * Called before a panel is initalized + */ +export type PanelTypeChangedHook = ( + options: Partial, + prevPluginId?: string, + prevOptions?: any +) => Partial; export class ReactPanelPlugin { panel: ComponentClass>; editor?: ComponentClass>; defaults?: TOptions; - preserveOptions?: PreservePanelOptionsHandler; + + panelTypeChangedHook?: PanelTypeChangedHook; constructor(panel: ComponentClass>) { this.panel = panel; @@ -46,8 +54,12 @@ export class ReactPanelPlugin { this.defaults = defaults; } - setPreserveOptionsHandler(handler: PreservePanelOptionsHandler) { - this.preserveOptions = handler; + /** + * Called when the visualization changes. + * Lets you keep whatever settings made sense in the previous panel + */ + setPanelTypeChangedHook(v: PanelTypeChangedHook) { + this.panelTypeChangedHook = v; } } diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 3093aec9957..f33b3f4019e 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -92,14 +92,29 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { messageType = AlertStateRecovery } + fields := make(map[string]interface{}, 0) + fieldLimitCount := 4 + for index, evt := range evalContext.EvalMatches { + fields[evt.Metric] = evt.Value + if index > fieldLimitCount { + break + } + } + bodyJSON := simplejson.New() bodyJSON.Set("message_type", messageType) bodyJSON.Set("entity_id", evalContext.Rule.Name) + bodyJSON.Set("entity_display_name", evalContext.GetNotificationTitle()) bodyJSON.Set("timestamp", time.Now().Unix()) bodyJSON.Set("state_start_time", evalContext.StartTime.Unix()) bodyJSON.Set("state_message", evalContext.Rule.Message) bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion) bodyJSON.Set("alert_url", ruleUrl) + bodyJSON.Set("metrics", fields) + + if evalContext.Error != nil { + bodyJSON.Set("error_message", evalContext.Error.Error()) + } if evalContext.ImagePublicUrl != "" { bodyJSON.Set("image_url", evalContext.ImagePublicUrl) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 3955f45d926..029fb0e8371 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -14,6 +14,7 @@ import { PanelEditor } from '../panel_editor/PanelEditor'; import { PanelModel, DashboardModel } from '../state'; import { PanelPlugin } from 'app/types'; import { PanelResizer } from './PanelResizer'; +import { PanelTypeChangedHook } from '@grafana/ui'; export interface Props { panel: PanelModel; @@ -91,7 +92,11 @@ export class DashboardPanel extends PureComponent { this.props.panel.changeType(pluginId); } else { - panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions); + let hook: PanelTypeChangedHook | null = null; + if (plugin.exports.reactPanel) { + hook = plugin.exports.reactPanel.panelTypeChangedHook; + } + panel.changeType(pluginId, hook); } } diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 88065fdf208..128bd8d0785 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { DataQuery, TimeSeries, Threshold, ScopedVars } from '@grafana/ui'; +import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui'; import { TableData } from '@grafana/ui/src'; export interface GridPos { @@ -237,7 +237,7 @@ export class PanelModel { }); } - changeType(pluginId: string, preserveOptions?: any) { + changeType(pluginId: string, hook?: PanelTypeChangedHook) { const oldOptions: any = this.getOptionsToRemember(); const oldPluginId = this.type; @@ -255,9 +255,12 @@ export class PanelModel { this.cachedPluginOptions[oldPluginId] = oldOptions; this.restorePanelOptions(pluginId); - if (preserveOptions && oldOptions) { + // Callback that can validate and migrate any existing settings + if (hook) { this.options = this.options || {}; - Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options)); + const old = oldOptions ? oldOptions.options : null; + + Object.assign(this.options, hook(this.options, oldPluginId, old)); } } diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index 6a2a7300058..f3dab902dd1 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -3,10 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui'; import { BarGaugePanel } from './BarGaugePanel'; import { BarGaugePanelEditor } from './BarGaugePanelEditor'; import { BarGaugeOptions, defaults } from './types'; -import { gaugePreserveOptionsHandler } from '../gauge/module'; +import { gaugePanelTypeChangedHook } from '../gauge/module'; export const reactPanel = new ReactPanelPlugin(BarGaugePanel); reactPanel.setEditor(BarGaugePanelEditor); reactPanel.setDefaults(defaults); -reactPanel.setPreserveOptionsHandler(gaugePreserveOptionsHandler); +reactPanel.setPanelTypeChangedHook(gaugePanelTypeChangedHook); diff --git a/public/app/plugins/panel/gauge/module.test.ts b/public/app/plugins/panel/gauge/module.test.ts new file mode 100644 index 00000000000..8dc29d42643 --- /dev/null +++ b/public/app/plugins/panel/gauge/module.test.ts @@ -0,0 +1,27 @@ +import { gaugePanelTypeChangedHook } from './module'; + +describe('Gauge Module', () => { + describe('migrations', () => { + it('should migrate from 6.0 settings to 6.1', () => { + const v60 = { + minValue: 50, + maxValue: 60, + showThresholdMarkers: true, + showThresholdLabels: false, + valueOptions: { + prefix: 'a', + suffix: 'z', + decimals: 4, + stat: 'avg', + unit: 'ms', + }, + valueMappings: [], + thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }], + }; + + const after = gaugePanelTypeChangedHook(v60); + expect((after.stat = 'avg')); + expect(after).toMatchSnapshot(); + }); + }); +}); diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 018a1403b37..e33d8f0a878 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,4 +1,4 @@ -import { ReactPanelPlugin } from '@grafana/ui'; +import { ReactPanelPlugin, DisplayValueOptions } from '@grafana/ui'; import cloneDeep from 'lodash/cloneDeep'; import { GaugePanelEditor } from './GaugePanelEditor'; @@ -8,9 +8,10 @@ import { GaugeOptions, defaults } from './types'; export const reactPanel = new ReactPanelPlugin(GaugePanel); // Bar Gauge uses the same handler -export const gaugePreserveOptionsHandler = (pluginId: string, prevOptions: any) => { - const options: Partial = {}; +const optionsToCheck = ['display', 'stat', 'maxValue', 'maxValue']; + +export const gaugePanelTypeChangedHook = (options: Partial, prevPluginId?: string, prevOptions?: any) => { // TODO! migrate to new settings format // // thresholds?: Threshold[]; @@ -21,11 +22,32 @@ export const gaugePreserveOptionsHandler = (pluginId: string, prevOptions: any) // console.warn('TODO!! how do we best migration options?'); // } - if (prevOptions.display) { - options.stat = prevOptions.stat; - options.display = cloneDeep(prevOptions.display); - options.maxValue = prevOptions.maxValue; - options.minValue = prevOptions.minValue; + // 6.0 -> 6.1, settings were stored on the root, now moved to display + if (!options.display && !prevOptions && options.hasOwnProperty('thresholds')) { + console.log('Migrating old gauge settings format', options); + const migrate = options as any; + const display = (migrate.valueOptions || {}) as DisplayValueOptions; + + display.thresholds = migrate.thresholds; + display.mappings = migrate.valueMappings; + if (migrate.valueMappings) { + options.stat = migrate.valueMappings.stat; + delete migrate.valueMappings.stat; + } + + delete migrate.valueOptions; + delete migrate.thresholds; + delete migrate.valueMappings; + + options.display = display; + } + + if (prevOptions) { + optionsToCheck.forEach(v => { + if (prevOptions.hasOwnProperty(v)) { + options[v] = cloneDeep(prevOptions.display); + } + }); } return options; @@ -33,4 +55,4 @@ export const gaugePreserveOptionsHandler = (pluginId: string, prevOptions: any) reactPanel.setEditor(GaugePanelEditor); reactPanel.setDefaults(defaults); -reactPanel.setPreserveOptionsHandler(gaugePreserveOptionsHandler); +reactPanel.setPanelTypeChangedHook(gaugePanelTypeChangedHook); diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index bac292e8c29..b2e3057de34 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -8,3 +8,10 @@ export const reactPanel = new ReactPanelPlugin(TextPanel); reactPanel.setEditor(TextPanelEditor); reactPanel.setDefaults(defaults); +reactPanel.setPanelTypeChangedHook((options: TextOptions, prevPluginId: string, prevOptions: any) => { + if (prevPluginId === 'text') { + return prevOptions as TextOptions; + } + + return options; +});