diff --git a/src/components/grafana/builder.ts b/src/components/grafana/builder.ts index a930077a..718dd667 100644 --- a/src/components/grafana/builder.ts +++ b/src/components/grafana/builder.ts @@ -6,11 +6,12 @@ import { XRayConnection, } from './connections'; import { Grafana } from './grafana'; +import type { GrafanaDashboardBuilder } from './dashboards/builder'; export class GrafanaBuilder { private readonly name: string; - private readonly connectionBuilders: GrafanaConnection.ConnectionBuilder[] = - []; + private readonly connectionBuilders: GrafanaConnection.Builder[] = []; + private readonly dashboardBuilders: GrafanaDashboardBuilder.Dashboard[] = []; constructor(name: string) { this.name = name; @@ -39,16 +40,28 @@ export class GrafanaBuilder { return this; } - public addConnection(builder: GrafanaConnection.ConnectionBuilder): this { + public addConnection(builder: GrafanaConnection.Builder): this { this.connectionBuilders.push(builder); return this; } + public addDashboard(dashboard: GrafanaDashboardBuilder.Dashboard): this { + this.dashboardBuilders.push(dashboard); + + return this; + } + public build(opts: pulumi.ComponentResourceOptions = {}): Grafana { if (!this.connectionBuilders.length) { throw new Error( - 'At least one connection is required. Call addConnection() to add custom connection or use one of existing connection builders.', + 'At least one connection is required. Call addConnection() to add custom connection or use one of existing connection builders.', + ); + } + + if (!this.dashboardBuilders.length) { + throw new Error( + 'At least one dashboard is required. Call addDashboard() to add a dashboard.', ); } @@ -56,6 +69,7 @@ export class GrafanaBuilder { this.name, { connectionBuilders: this.connectionBuilders, + dashboardBuilders: this.dashboardBuilders, }, opts, ); diff --git a/src/components/grafana/connections/connection.ts b/src/components/grafana/connections/connection.ts index 98efc36a..fea8e342 100644 --- a/src/components/grafana/connections/connection.ts +++ b/src/components/grafana/connections/connection.ts @@ -10,7 +10,7 @@ export namespace GrafanaConnection { awsAccountId: string; }; - export type ConnectionBuilder = ( + export type Builder = ( opts: pulumi.ComponentResourceOptions, ) => GrafanaConnection; } diff --git a/src/components/grafana/dashboards/builder.ts b/src/components/grafana/dashboards/builder.ts new file mode 100644 index 00000000..da79b56a --- /dev/null +++ b/src/components/grafana/dashboards/builder.ts @@ -0,0 +1,75 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as grafana from '@pulumiverse/grafana'; +import { GrafanaConnection } from '../connections'; +import { PanelBuilder } from '../panels/types'; +import { mergeWithDefaults } from '../../../shared/merge-with-defaults'; + +export namespace GrafanaDashboardBuilder { + export type Config = { + timezone?: string; + refresh?: string; + }; + + export type Dashboard = ( + connections: GrafanaConnection[], + folder?: grafana.oss.Folder, + opts?: pulumi.ComponentResourceOptions, + ) => grafana.oss.Dashboard; +} + +const defaults = { + timezone: 'browser', + refresh: '10s', +}; + +export class GrafanaDashboardBuilder { + private readonly name: string; + private readonly title: string; + private readonly panelBuilders: PanelBuilder[] = []; + private configuration: GrafanaDashboardBuilder.Config = {}; + + constructor(name: string, title: string) { + this.name = name; + this.title = title; + } + + withConfig(options: GrafanaDashboardBuilder.Config): this { + this.configuration = options; + + return this; + } + + addPanel(builder: PanelBuilder): this { + this.panelBuilders.push(builder); + + return this; + } + + build(): GrafanaDashboardBuilder.Dashboard { + if (!this.panelBuilders.length) { + throw new Error( + 'At least one panel is required. Call addPanel() to add a panel.', + ); + } + + const { name, title, panelBuilders } = this; + const options = mergeWithDefaults(defaults, this.configuration); + + return (connections, folder, opts) => { + const panels = panelBuilders.map(build => build(connections)); + return new grafana.oss.Dashboard( + name, + { + folder: folder?.uid, + configJson: pulumi.jsonStringify({ + title, + timezone: options.timezone, + refresh: options.refresh, + panels, + }), + }, + opts, + ); + }; + } +} diff --git a/src/components/grafana/dashboards/index.ts b/src/components/grafana/dashboards/index.ts index 6e0be25c..86b63470 100644 --- a/src/components/grafana/dashboards/index.ts +++ b/src/components/grafana/dashboards/index.ts @@ -1,2 +1,2 @@ -export { default as WebServerSloDashboardBuilder } from './web-server-slo'; -export * as panel from './panels'; +export { GrafanaDashboardBuilder as DashboardBuilder } from './builder'; +export { createWebServerSloDashboard } from './web-server-slo'; diff --git a/src/components/grafana/dashboards/types.ts b/src/components/grafana/dashboards/types.ts deleted file mode 100644 index 1a561bee..00000000 --- a/src/components/grafana/dashboards/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as pulumi from '@pulumi/pulumi'; -import * as grafana from '@pulumiverse/grafana'; - -// TODO: Should we prefix all namespaces with `Studion` -export namespace Grafana { - // TODO: Create SLO abstraction that enables configuring: - // - panels (long-window SLI, long-window error budget) - // - alerts (long-window burn, short-window burn) - export type Threshold = { - value: number | null; - color: string; - }; - export type Metric = { - label: string; - query: string; - thresholds: Threshold[]; - }; - - export type Args = { - title: pulumi.Input; - provider: pulumi.Input; - tags: pulumi.Input[]>; - }; - - export type Panel = { - title: string; - gridPos: Panel.Position; - type: string; - datasource: string; - targets: { - expr: string; - legendFormat: string; - }[]; - fieldConfig: { - defaults: { - unit?: string; - min?: number; - max?: number; - color?: { - mode: string; - }; - thresholds?: { - mode: string; - steps: Threshold[]; - }; - custom?: { - lineInterpolation?: string; - spanNulls: boolean; - }; - }; - }; - options?: { - colorMode?: string; - graphMode?: string; - justifyMode?: string; - textMode?: string; - reduceOptions?: { - calcs?: string[]; - fields?: string; - values?: boolean; - }; - }; - }; - - export namespace Panel { - export type Position = { - x: number; - y: number; - w: number; - h: number; - }; - } -} diff --git a/src/components/grafana/dashboards/web-server-slo.ts b/src/components/grafana/dashboards/web-server-slo.ts index 94ae90ea..c7056f36 100644 --- a/src/components/grafana/dashboards/web-server-slo.ts +++ b/src/components/grafana/dashboards/web-server-slo.ts @@ -1,234 +1,49 @@ -import * as pulumi from '@pulumi/pulumi'; -import * as grafana from '@pulumiverse/grafana'; +import { mergeWithDefaults } from '../../../shared/merge-with-defaults'; +import { GrafanaDashboardBuilder } from './builder'; import { queries as promQ } from '../../prometheus'; -import { Grafana } from './types'; import { - createBurnRatePanel, - createStatPercentagePanel, - createTimeSeriesPanel, - createTimeSeriesPercentagePanel, -} from './panels'; - -class WebServerSloDashboardBuilder { + createAvailabilityPanel, + createAvailabilityBurnRatePanel, +} from '../panels/availability'; +import { + createSuccessRatePanel, + createSuccessRateTimeSeriesPanel, + createSuccessRateBurnRatePanel, +} from '../panels/success-rate'; +import { + createLatencyPanel, + createLatencyPercentilePanel, + createLatencyPercentagePanel, + createLatencyBurnRatePanel, +} from '../panels/latency'; + +const defaults = { + target: 0.99, + window: '30d', + shortWindow: '5m', + targetLatency: 250, +}; + +export function createWebServerSloDashboard(config: { name: string; - title: pulumi.Output; - panels: Grafana.Panel[] = []; - tags?: pulumi.Output; - - constructor(name: string, args: Grafana.Args) { - this.name = name; - this.title = pulumi.output(args.title); - } - - withAvailability( - target: number, - window: promQ.TimeRange, - dataSource: string, - prometheusNamespace: string, - ): this { - const availabilityPercentage = promQ.getAvailabilityPercentageQuery( - prometheusNamespace, - window, - ); - const availabilityBurnRate = promQ.getBurnRateQuery( - promQ.getAvailabilityQuery(prometheusNamespace, '1h'), - target, - ); - - const availabilitySloPanel = createStatPercentagePanel( - 'Availability', - { x: 0, y: 0, w: 8, h: 8 }, - dataSource, - { - label: 'Availability', - query: availabilityPercentage, - thresholds: [], - }, - ); - const availabilityBurnRatePanel = createBurnRatePanel( - 'Availability Burn Rate', - { x: 0, y: 8, w: 8, h: 4 }, - dataSource, - { - label: 'Burn Rate', - query: availabilityBurnRate, - thresholds: [], - }, - ); - - this.panels.push(availabilitySloPanel, availabilityBurnRatePanel); - - return this; - } - - withSuccessRate( - target: number, - window: promQ.TimeRange, - shortWindow: promQ.TimeRange, - filter: string, - dataSource: string, - prometheusNamespace: string, - ): this { - const successRateSlo = promQ.getSuccessPercentageQuery( - prometheusNamespace, - window, - filter, - ); - const successRateBurnRate = promQ.getBurnRateQuery( - promQ.getSuccessRateQuery(prometheusNamespace, '1h', filter), - target, - ); - const successRate = promQ.getSuccessPercentageQuery( - prometheusNamespace, - shortWindow, - filter, - ); - - const successRateSloPanel = createStatPercentagePanel( - 'Success Rate', - { x: 8, y: 0, w: 8, h: 8 }, - dataSource, - { - label: 'Success Rate', - query: successRateSlo, - thresholds: [], - }, - ); - const successRatePanel = createTimeSeriesPercentagePanel( - 'HTTP Request Success Rate', - { x: 0, y: 16, w: 12, h: 8 }, - dataSource, - { - label: 'Success Rate', - query: successRate, - thresholds: [], - }, - ); - const successRateBurnRatePanel = createBurnRatePanel( - 'Success Rate Burn Rate', - { x: 8, y: 8, w: 8, h: 4 }, - dataSource, - { - label: 'Burn Rate', - query: successRateBurnRate, - thresholds: [], - }, - ); - - this.panels.push( - successRateSloPanel, - successRatePanel, - successRateBurnRatePanel, - ); - - return this; - } - - withLatency( - target: number, - targetLatency: number, - window: promQ.TimeRange, - shortWindow: promQ.TimeRange, - filter: string, - dataSource: string, - prometheusNamespace: string, - ): this { - const latencySlo = promQ.getLatencyPercentageQuery( - prometheusNamespace, - window, - targetLatency, - filter, - ); - const latencyBurnRate = promQ.getBurnRateQuery( - promQ.getLatencyRateQuery(prometheusNamespace, '1h', targetLatency), - target, - ); - const percentileLatency = promQ.getPercentileLatencyQuery( - prometheusNamespace, - shortWindow, - target, - filter, - ); - const latencyBelowThreshold = promQ.getLatencyPercentageQuery( - prometheusNamespace, - shortWindow, - targetLatency, - filter, - ); - - const latencySloPanel = createStatPercentagePanel( - 'Request % below 250ms', - { x: 16, y: 0, w: 8, h: 8 }, - dataSource, - { - label: 'Request % below 250ms', - query: latencySlo, - thresholds: [], - }, - ); - const percentileLatencyPanel = createTimeSeriesPanel( - '99th Percentile Latency', - { x: 12, y: 16, w: 12, h: 8 }, - dataSource, - { - label: '99th Percentile Latency', - query: percentileLatency, - thresholds: [], - }, - 'ms', - ); - const latencyPercentagePanel = createTimeSeriesPercentagePanel( - 'Request percentage below 250ms', - { x: 0, y: 24, w: 12, h: 8 }, - dataSource, - { - label: 'Request percentage below 250ms', - query: latencyBelowThreshold, - thresholds: [], - }, - ); - const latencyBurnRatePanel = createBurnRatePanel( - 'Latency Burn Rate', - { x: 16, y: 8, w: 8, h: 4 }, - dataSource, - { - label: 'Burn Rate', - query: latencyBurnRate, - thresholds: [], - }, - ); - - this.panels.push( - latencySloPanel, - percentileLatencyPanel, - latencyPercentagePanel, - latencyBurnRatePanel, - ); - - return this; - } - - build( - provider: pulumi.Output, - ): pulumi.Output { - return pulumi - .all([this.title, this.panels, provider, this.tags]) - .apply(([title, panels, provider, tags]) => { - return new grafana.oss.Dashboard( - this.name, - { - configJson: JSON.stringify({ - title, - tags, - timezone: 'browser', - refresh: '10s', - panels, - }), - }, - { provider }, - ); - }); - } + title: string; + ampNamespace: string; + filter: string; + target?: number; + window?: promQ.TimeRange; + shortWindow?: promQ.TimeRange; + targetLatency?: number; +}): GrafanaDashboardBuilder.Dashboard { + const argsWithDefaults = mergeWithDefaults(defaults, config); + return new GrafanaDashboardBuilder(config.name, argsWithDefaults.title) + .addPanel(createAvailabilityPanel(argsWithDefaults)) + .addPanel(createAvailabilityBurnRatePanel(argsWithDefaults)) + .addPanel(createSuccessRatePanel(argsWithDefaults)) + .addPanel(createSuccessRateTimeSeriesPanel(argsWithDefaults)) + .addPanel(createSuccessRateBurnRatePanel(argsWithDefaults)) + .addPanel(createLatencyPanel(argsWithDefaults)) + .addPanel(createLatencyPercentilePanel(argsWithDefaults)) + .addPanel(createLatencyPercentagePanel(argsWithDefaults)) + .addPanel(createLatencyBurnRatePanel(argsWithDefaults)) + .build(); } - -export default WebServerSloDashboardBuilder; diff --git a/src/components/grafana/grafana.ts b/src/components/grafana/grafana.ts index a81bdf5d..8685065b 100644 --- a/src/components/grafana/grafana.ts +++ b/src/components/grafana/grafana.ts @@ -1,15 +1,19 @@ import * as pulumi from '@pulumi/pulumi'; +import * as grafana from '@pulumiverse/grafana'; +import type { GrafanaDashboardBuilder } from './dashboards/builder'; import { GrafanaConnection } from './connections'; export namespace Grafana { export type Args = { - connectionBuilders: GrafanaConnection.ConnectionBuilder[]; + connectionBuilders: GrafanaConnection.Builder[]; + dashboardBuilders: GrafanaDashboardBuilder.Dashboard[]; }; } export class Grafana extends pulumi.ComponentResource { public readonly name: string; public readonly connections: GrafanaConnection[]; + public readonly dashboards: grafana.oss.Dashboard[]; constructor( name: string, @@ -20,10 +24,20 @@ export class Grafana extends pulumi.ComponentResource { this.name = name; - this.connections = args.connectionBuilders.map(build => - build({ parent: this }), + this.connections = args.connectionBuilders.map(build => { + return build({ parent: this }); + }); + + const folder = new grafana.oss.Folder( + name, + { title: name }, + { parent: this }, ); + this.dashboards = args.dashboardBuilders.map(build => { + return build(this.connections, folder, { parent: folder }); + }); + this.registerOutputs(); } } diff --git a/src/components/grafana/index.ts b/src/components/grafana/index.ts index ee49c030..ae96364f 100644 --- a/src/components/grafana/index.ts +++ b/src/components/grafana/index.ts @@ -1,4 +1,5 @@ export * as dashboard from './dashboards'; +export * as panels from './panels'; export { GrafanaConnection, AMPConnection, diff --git a/src/components/grafana/panels/availability.ts b/src/components/grafana/panels/availability.ts new file mode 100644 index 00000000..92ba1083 --- /dev/null +++ b/src/components/grafana/panels/availability.ts @@ -0,0 +1,54 @@ +import { queries as promQ } from '../../prometheus'; +import { AMPConnection } from '../connections'; +import { PanelBuilder } from './types'; +import { + createStatPercentagePanel, + createBurnRatePanel, + requireConnection, +} from './helpers'; + +export function createAvailabilityPanel(config: { + target: number; + window: promQ.TimeRange; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createStatPercentagePanel( + 'Availability', + { x: 0, y: 0, w: 8, h: 8 }, + ds, + { + label: 'Availability', + query: promQ.getAvailabilityPercentageQuery( + config.ampNamespace, + config.window, + ), + thresholds: [], + }, + ); + }; +} + +export function createAvailabilityBurnRatePanel(config: { + target: number; + window: promQ.TimeRange; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createBurnRatePanel( + 'Availability Burn Rate', + { x: 0, y: 8, w: 8, h: 4 }, + ds, + { + label: 'Burn Rate', + query: promQ.getBurnRateQuery( + promQ.getAvailabilityQuery(config.ampNamespace, '1h'), + config.target, + ), + thresholds: [], + }, + ); + }; +} diff --git a/src/components/grafana/dashboards/panels.ts b/src/components/grafana/panels/helpers.ts similarity index 72% rename from src/components/grafana/dashboards/panels.ts rename to src/components/grafana/panels/helpers.ts index c410cd6c..8ac9ecb3 100644 --- a/src/components/grafana/dashboards/panels.ts +++ b/src/components/grafana/panels/helpers.ts @@ -1,4 +1,6 @@ -import { Grafana } from './types'; +import * as pulumi from '@pulumi/pulumi'; +import { GrafanaConnection } from '../connections'; +import { Panel, Metric } from './types'; const percentageFieldConfig = { unit: 'percent', @@ -8,10 +10,10 @@ const percentageFieldConfig = { export function createStatPercentagePanel( title: string, - position: Grafana.Panel.Position, - dataSource: string, - metric: Grafana.Metric, -): Grafana.Panel { + position: Panel.Position, + dataSource: pulumi.Input, + metric: Metric, +): Panel { return { title, gridPos: position, @@ -41,10 +43,10 @@ export function createStatPercentagePanel( export function createTimeSeriesPercentagePanel( title: string, - position: Grafana.Panel.Position, - dataSource: string, - metric: Grafana.Metric, -): Grafana.Panel { + position: Panel.Position, + dataSource: pulumi.Input, + metric: Metric, +): Panel { return createTimeSeriesPanel( title, position, @@ -58,13 +60,13 @@ export function createTimeSeriesPercentagePanel( export function createTimeSeriesPanel( title: string, - position: Grafana.Panel.Position, - dataSource: string, - metric: Grafana.Metric, + position: Panel.Position, + dataSource: pulumi.Input, + metric: Metric, unit?: string, min?: number, max?: number, -): Grafana.Panel { +): Panel { return { title, type: 'timeseries', @@ -96,10 +98,10 @@ export function createTimeSeriesPanel( export function createBurnRatePanel( title: string, - position: Grafana.Panel.Position, - dataSource: string, - metric: Grafana.Metric, -): Grafana.Panel { + position: Panel.Position, + dataSource: pulumi.Input, + metric: Metric, +): Panel { return { type: 'stat', title, @@ -136,3 +138,13 @@ export function createBurnRatePanel( }, }; } + +export function requireConnection( + connections: GrafanaConnection[], + connectionType: new (...args: any[]) => T, +): T { + const connection = connections.find(c => c instanceof connectionType); + if (!connection) + throw new Error(`Required connection ${connectionType.name} not found`); + return connection as T; +} diff --git a/src/components/grafana/panels/index.ts b/src/components/grafana/panels/index.ts new file mode 100644 index 00000000..bbee81b7 --- /dev/null +++ b/src/components/grafana/panels/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './helpers'; +export * from './availability'; +export * from './success-rate'; +export * from './latency'; diff --git a/src/components/grafana/panels/latency.ts b/src/components/grafana/panels/latency.ts new file mode 100644 index 00000000..1af643e1 --- /dev/null +++ b/src/components/grafana/panels/latency.ts @@ -0,0 +1,117 @@ +import { queries as promQ } from '../../prometheus'; +import { AMPConnection } from '../connections'; +import { PanelBuilder } from './types'; +import { + createStatPercentagePanel, + createTimeSeriesPanel, + createTimeSeriesPercentagePanel, + createBurnRatePanel, + requireConnection, +} from './helpers'; + +export function createLatencyPanel(config: { + target: number; + window: promQ.TimeRange; + targetLatency: number; + filter: string; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createStatPercentagePanel( + 'Request % below 250ms', + { x: 16, y: 0, w: 8, h: 8 }, + ds, + { + label: 'Request % below 250ms', + query: promQ.getLatencyPercentageQuery( + config.ampNamespace, + config.window, + config.targetLatency, + config.filter, + ), + thresholds: [], + }, + ); + }; +} + +export function createLatencyPercentilePanel(config: { + target: number; + shortWindow: promQ.TimeRange; + filter: string; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createTimeSeriesPanel( + '99th Percentile Latency', + { x: 12, y: 16, w: 12, h: 8 }, + ds, + { + label: '99th Percentile Latency', + query: promQ.getPercentileLatencyQuery( + config.ampNamespace, + config.shortWindow, + config.target, + config.filter, + ), + thresholds: [], + }, + 'ms', + ); + }; +} + +export function createLatencyPercentagePanel(config: { + targetLatency: number; + shortWindow: promQ.TimeRange; + filter: string; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createTimeSeriesPercentagePanel( + 'Request percentage below 250ms', + { x: 0, y: 24, w: 12, h: 8 }, + ds, + { + label: 'Request percentage below 250ms', + query: promQ.getLatencyPercentageQuery( + config.ampNamespace, + config.shortWindow, + config.targetLatency, + config.filter, + ), + thresholds: [], + }, + ); + }; +} + +export function createLatencyBurnRatePanel(config: { + target: number; + targetLatency: number; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createBurnRatePanel( + 'Latency Burn Rate', + { x: 16, y: 8, w: 8, h: 4 }, + ds, + { + label: 'Burn Rate', + query: promQ.getBurnRateQuery( + promQ.getLatencyRateQuery( + config.ampNamespace, + '1h', + config.targetLatency, + ), + config.target, + ), + thresholds: [], + }, + ); + }; +} diff --git a/src/components/grafana/panels/success-rate.ts b/src/components/grafana/panels/success-rate.ts new file mode 100644 index 00000000..904830c5 --- /dev/null +++ b/src/components/grafana/panels/success-rate.ts @@ -0,0 +1,81 @@ +import { queries as promQ } from '../../prometheus'; +import { AMPConnection } from '../connections'; +import { PanelBuilder } from './types'; +import { + createStatPercentagePanel, + createTimeSeriesPercentagePanel, + createBurnRatePanel, + requireConnection, +} from './helpers'; + +export function createSuccessRatePanel(config: { + target: number; + window: promQ.TimeRange; + filter: string; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createStatPercentagePanel( + 'Success Rate', + { x: 8, y: 0, w: 8, h: 8 }, + ds, + { + label: 'Success Rate', + query: promQ.getSuccessPercentageQuery( + config.ampNamespace, + config.window, + config.filter, + ), + thresholds: [], + }, + ); + }; +} + +export function createSuccessRateTimeSeriesPanel(config: { + shortWindow: promQ.TimeRange; + filter: string; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createTimeSeriesPercentagePanel( + 'HTTP Request Success Rate', + { x: 0, y: 16, w: 12, h: 8 }, + ds, + { + label: 'Success Rate', + query: promQ.getSuccessPercentageQuery( + config.ampNamespace, + config.shortWindow, + config.filter, + ), + thresholds: [], + }, + ); + }; +} + +export function createSuccessRateBurnRatePanel(config: { + target: number; + filter: string; + ampNamespace: string; +}): PanelBuilder { + return connections => { + const ds = requireConnection(connections, AMPConnection).dataSource.name; + return createBurnRatePanel( + 'Success Rate Burn Rate', + { x: 8, y: 8, w: 8, h: 4 }, + ds, + { + label: 'Burn Rate', + query: promQ.getBurnRateQuery( + promQ.getSuccessRateQuery(config.ampNamespace, '1h', config.filter), + config.target, + ), + thresholds: [], + }, + ); + }; +} diff --git a/src/components/grafana/panels/types.ts b/src/components/grafana/panels/types.ts new file mode 100644 index 00000000..83f5fed1 --- /dev/null +++ b/src/components/grafana/panels/types.ts @@ -0,0 +1,64 @@ +import * as pulumi from '@pulumi/pulumi'; +import { GrafanaConnection } from '../connections'; + +export type Panel = { + title: string; + gridPos: Panel.Position; + type: string; + datasource: pulumi.Input; + targets: { + expr: string; + legendFormat: string; + }[]; + fieldConfig: { + defaults: { + unit?: string; + min?: number; + max?: number; + color?: { + mode: string; + }; + thresholds?: { + mode: string; + steps: Threshold[]; + }; + custom?: { + lineInterpolation?: string; + spanNulls: boolean; + }; + }; + }; + options?: { + colorMode?: string; + graphMode?: string; + justifyMode?: string; + textMode?: string; + reduceOptions?: { + calcs?: string[]; + fields?: string; + values?: boolean; + }; + }; +}; + +export namespace Panel { + export type Position = { + x: number; + y: number; + w: number; + h: number; + }; +} + +export type Metric = { + label: string; + query: string; + thresholds: Threshold[]; +}; + +export type Threshold = { + value: number | null; + color: string; +}; + +export type PanelBuilder = (connections: GrafanaConnection[]) => Panel; diff --git a/tests/acm-certificate/test-context.ts b/tests/acm-certificate/test-context.ts index af80c957..5f1b3f7b 100644 --- a/tests/acm-certificate/test-context.ts +++ b/tests/acm-certificate/test-context.ts @@ -25,4 +25,6 @@ interface AwsContext { } export interface AcmCertificateTestContext - extends ConfigContext, PulumiProgramContext, AwsContext {} + extends ConfigContext, + PulumiProgramContext, + AwsContext {} diff --git a/tests/redis/test-context.ts b/tests/redis/test-context.ts index 51ed8db5..83629fb3 100644 --- a/tests/redis/test-context.ts +++ b/tests/redis/test-context.ts @@ -31,4 +31,6 @@ interface AwsContext { } export interface RedisTestContext - extends ConfigContext, PulumiProgramContext, AwsContext {} + extends ConfigContext, + PulumiProgramContext, + AwsContext {} diff --git a/tests/web-server/test-context.ts b/tests/web-server/test-context.ts index e04050ee..4086204f 100644 --- a/tests/web-server/test-context.ts +++ b/tests/web-server/test-context.ts @@ -42,4 +42,6 @@ interface AwsContext { } export interface WebServerTestContext - extends ConfigContext, PulumiProgramContext, AwsContext {} + extends ConfigContext, + PulumiProgramContext, + AwsContext {}