From b7f384f30ce9689886261985fcc441efacf3a52a Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 14 Oct 2021 18:31:52 -0400
Subject: [PATCH] [Alerting] Show execution duration on Rule Details view
 (#114719) (#115092)

* Adding execution duration to get alert instance summary

* Showing execution duration on rule details view

* Fixing unit tests

* Updating to match new mockup

* Fixing types

* Fixing functional test

* Removing unneeded max and min and adding tests

* Removing unneeded max and min and adding tests

* Fixing functional test

* Adding left axis

* PR feedback

* Reducing chart height

* PR feedback

* PR feedback

* PR feedback

Co-authored-by: ymao1 <ying.mao@elastic.co>
---
 .../alerting/common/alert_instance_summary.ts |   4 +
 ...rt_instance_summary_from_event_log.test.ts |  70 +++++++--
 .../alert_instance_summary_from_event_log.ts  |  19 +++
 .../routes/get_rule_alert_summary.test.ts     |   4 +
 .../server/routes/get_rule_alert_summary.ts   |   2 +
 .../legacy/get_alert_instance_summary.test.ts |   4 +
 .../tests/get_alert_instance_summary.test.ts  |  11 +-
 .../lib/alert_api/alert_summary.test.ts       |   8 ++
 .../lib/alert_api/alert_summary.ts            |   2 +
 .../lib/execution_duration_utils.test.ts      |  68 +++++++++
 .../lib/execution_duration_utils.ts           |  38 +++++
 .../components/alert_instances.scss           |   5 +
 .../components/alert_instances.test.tsx       | 120 ++++++++++++++++
 .../components/alert_instances.tsx            | 112 ++++++++++++++-
 .../components/alert_instances_route.test.tsx |   4 +
 .../alerts_list/components/alerts_list.tsx    |  28 ++--
 .../execution_duration_chart.test.tsx         |  72 ++++++++++
 .../components/execution_duration_chart.tsx   | 133 ++++++++++++++++++
 .../alerting/get_alert_instance_summary.ts    |   1 +
 .../alerting/get_alert_instance_summary.ts    |  14 +-
 20 files changed, 685 insertions(+), 34 deletions(-)
 create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.test.ts
 create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.ts
 create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx
 create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx

diff --git a/x-pack/plugins/alerting/common/alert_instance_summary.ts b/x-pack/plugins/alerting/common/alert_instance_summary.ts
index 7ae37d6ddf53a..462d20b8fb2ea 100644
--- a/x-pack/plugins/alerting/common/alert_instance_summary.ts
+++ b/x-pack/plugins/alerting/common/alert_instance_summary.ts
@@ -23,6 +23,10 @@ export interface AlertInstanceSummary {
   lastRun?: string;
   errorMessages: Array<{ date: string; message: string }>;
   instances: Record<string, AlertInstanceStatus>;
+  executionDuration: {
+    average: number;
+    values: number[];
+  };
 }
 
 export interface AlertInstanceStatus {
diff --git a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts
index 22122b22fd3bb..a6529f4c30a7b 100644
--- a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts
+++ b/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts
@@ -5,6 +5,7 @@
  * 2.0.
  */
 
+import { random, mean } from 'lodash';
 import { SanitizedAlert, AlertInstanceSummary } from '../types';
 import { IValidatedEvent } from '../../../event_log/server';
 import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin';
@@ -31,6 +32,10 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "consumer": "alert-consumer",
         "enabled": false,
         "errorMessages": Array [],
+        "executionDuration": Object {
+          "average": 0,
+          "values": Array [],
+        },
         "id": "alert-123",
         "instances": Object {},
         "lastRun": undefined,
@@ -71,6 +76,10 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "consumer": "alert-consumer-2",
         "enabled": true,
         "errorMessages": Array [],
+        "executionDuration": Object {
+          "average": 0,
+          "values": Array [],
+        },
         "id": "alert-456",
         "instances": Object {},
         "lastRun": undefined,
@@ -137,7 +146,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {},
@@ -145,6 +154,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "OK",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('active alert with no instances but has errors', async () => {
@@ -163,7 +174,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, errorMessages, instances } = summary;
+    const { lastRun, status, errorMessages, instances, executionDuration } = summary;
     expect({ lastRun, status, errorMessages, instances }).toMatchInlineSnapshot(`
       Object {
         "errorMessages": Array [
@@ -181,6 +192,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Error",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with currently inactive instance', async () => {
@@ -202,7 +215,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -218,6 +231,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "OK",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('legacy alert with currently inactive instance', async () => {
@@ -239,7 +254,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -255,6 +270,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "OK",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with currently inactive instance, no new-instance', async () => {
@@ -275,7 +292,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -291,6 +308,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "OK",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with currently active instance', async () => {
@@ -312,7 +331,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -328,6 +347,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Active",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with currently active instance with no action group in event log', async () => {
@@ -349,7 +370,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -365,6 +386,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Active",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with currently active instance that switched action groups', async () => {
@@ -386,7 +409,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -402,6 +425,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Active",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with currently active instance, no new-instance', async () => {
@@ -422,7 +447,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -438,6 +463,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Active",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with active and inactive muted alerts', async () => {
@@ -462,7 +489,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -485,6 +512,8 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Active",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
 
   test('alert with active and inactive alerts over many executes', async () => {
@@ -515,7 +544,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
       dateEnd,
     });
 
-    const { lastRun, status, instances } = summary;
+    const { lastRun, status, instances, executionDuration } = summary;
     expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
       Object {
         "instances": Object {
@@ -538,7 +567,19 @@ describe('alertInstanceSummaryFromEventLog', () => {
         "status": "Active",
       }
     `);
+
+    testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration);
   });
+
+  const testExecutionDurations = (
+    actualDurations: number[],
+    executionDuration?: { average?: number; values?: number[] }
+  ) => {
+    expect(executionDuration).toEqual({
+      average: Math.round(mean(actualDurations)),
+      values: actualDurations,
+    });
+  };
 });
 
 function dateString(isoBaseDate: string, offsetMillis = 0): string {
@@ -573,6 +614,7 @@ export class EventsFactory {
       event: {
         provider: EVENT_LOG_PROVIDER,
         action: EVENT_LOG_ACTIONS.execute,
+        duration: random(2000, 180000) * 1000 * 1000,
       },
     };
 
@@ -634,6 +676,12 @@ export class EventsFactory {
     });
     return this;
   }
+
+  getExecutionDurations(): number[] {
+    return this.events
+      .filter((ev) => ev?.event?.action === 'execute' && ev?.event?.duration !== undefined)
+      .map((ev) => ev?.event?.duration! / (1000 * 1000));
+  }
 }
 
 function createAlert(overrides: Partial<SanitizedAlert>): SanitizedAlert<{ bar: boolean }> {
diff --git a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts
index 8a6c55dfac619..40fae121df51d 100644
--- a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts
+++ b/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts
@@ -5,10 +5,13 @@
  * 2.0.
  */
 
+import { mean } from 'lodash';
 import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types';
 import { IEvent } from '../../../event_log/server';
 import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin';
 
+const Millis2Nanos = 1000 * 1000;
+
 export interface AlertInstanceSummaryFromEventLogParams {
   alert: SanitizedAlert<{ bar: boolean }>;
   events: IEvent[];
@@ -36,9 +39,14 @@ export function alertInstanceSummaryFromEventLog(
     lastRun: undefined,
     errorMessages: [],
     instances: {},
+    executionDuration: {
+      average: 0,
+      values: [],
+    },
   };
 
   const instances = new Map<string, AlertInstanceStatus>();
+  const eventDurations: number[] = [];
 
   // loop through the events
   // should be sorted newest to oldest, we want oldest to newest, so reverse
@@ -66,6 +74,10 @@ export function alertInstanceSummaryFromEventLog(
         alertInstanceSummary.status = 'OK';
       }
 
+      if (event?.event?.duration) {
+        eventDurations.push(event?.event?.duration / Millis2Nanos);
+      }
+
       continue;
     }
 
@@ -111,6 +123,13 @@ export function alertInstanceSummaryFromEventLog(
 
   alertInstanceSummary.errorMessages.sort((a, b) => a.date.localeCompare(b.date));
 
+  if (eventDurations.length > 0) {
+    alertInstanceSummary.executionDuration = {
+      average: Math.round(mean(eventDurations)),
+      values: eventDurations,
+    };
+  }
+
   return alertInstanceSummary;
 }
 
diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts
index f0f34ae5756b4..5044c5c8617a0 100644
--- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts
+++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts
@@ -38,6 +38,10 @@ describe('getRuleAlertSummaryRoute', () => {
     status: 'OK',
     errorMessages: [],
     instances: {},
+    executionDuration: {
+      average: 1,
+      values: [3, 5, 5],
+    },
   };
 
   it('gets rule alert summary', async () => {
diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts
index 5bb8c5efd0d79..131bddcce049d 100644
--- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts
+++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts
@@ -39,6 +39,7 @@ const rewriteBodyRes: RewriteResponseCase<AlertInstanceSummary> = ({
   errorMessages,
   lastRun,
   instances: alerts,
+  executionDuration,
   ...rest
 }) => ({
   ...rest,
@@ -49,6 +50,7 @@ const rewriteBodyRes: RewriteResponseCase<AlertInstanceSummary> = ({
   status_end_date: statusEndDate,
   error_messages: errorMessages,
   last_run: lastRun,
+  execution_duration: executionDuration,
 });
 
 export const getRuleAlertSummaryRoute = (
diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts
index d529aec4162d6..ed2b3056da45e 100644
--- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts
+++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts
@@ -43,6 +43,10 @@ describe('getAlertInstanceSummaryRoute', () => {
     status: 'OK',
     errorMessages: [],
     instances: {},
+    executionDuration: {
+      average: 0,
+      values: [],
+    },
   };
 
   it('gets alert instance summary', async () => {
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts
index 28e432a78d6f9..2d8af69781c3b 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts
@@ -5,6 +5,7 @@
  * 2.0.
  */
 
+import { omit, mean } from 'lodash';
 import { RulesClient, ConstructorOptions } from '../rules_client';
 import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
 import { taskManagerMock } from '../../../../task_manager/server/mocks';
@@ -138,8 +139,11 @@ describe('getAlertInstanceSummary()', () => {
 
     const dateStart = new Date(Date.now() - 60 * 1000).toISOString();
 
+    const durations: number[] = eventsFactory.getExecutionDurations();
+
     const result = await rulesClient.getAlertInstanceSummary({ id: '1', dateStart });
-    expect(result).toMatchInlineSnapshot(`
+    const resultWithoutExecutionDuration = omit(result, 'executionDuration');
+    expect(resultWithoutExecutionDuration).toMatchInlineSnapshot(`
       Object {
         "alertTypeId": "123",
         "consumer": "alert-consumer",
@@ -182,6 +186,11 @@ describe('getAlertInstanceSummary()', () => {
         "throttle": null,
       }
     `);
+
+    expect(result.executionDuration).toEqual({
+      average: Math.round(mean(durations)),
+      values: durations,
+    });
   });
 
   // Further tests don't check the result of `getAlertInstanceSummary()`, as the result
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts
index c7b987f2b04bd..9f62e0ed0d50e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts
@@ -28,6 +28,10 @@ describe('loadAlertInstanceSummary', () => {
       statusStartDate: '2021-04-01T21:19:25.174Z',
       tags: [],
       throttle: null,
+      executionDuration: {
+        average: 0,
+        values: [],
+      },
     };
 
     http.get.mockResolvedValueOnce({
@@ -45,6 +49,10 @@ describe('loadAlertInstanceSummary', () => {
       status_start_date: '2021-04-01T21:19:25.174Z',
       tags: [],
       throttle: null,
+      execution_duration: {
+        average: 0,
+        values: [],
+      },
     });
 
     const result = await loadAlertInstanceSummary({ http, alertId: 'te/st' });
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts
index cb924db74cea5..701c8f3a7beec 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts
@@ -17,6 +17,7 @@ const rewriteBodyRes: RewriteRequestCase<AlertInstanceSummary> = ({
   status_end_date: statusEndDate,
   error_messages: errorMessages,
   last_run: lastRun,
+  execution_duration: executionDuration,
   ...rest
 }: any) => ({
   ...rest,
@@ -27,6 +28,7 @@ const rewriteBodyRes: RewriteRequestCase<AlertInstanceSummary> = ({
   errorMessages,
   lastRun,
   instances: alerts,
+  executionDuration,
 });
 
 export async function loadAlertInstanceSummary({
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.test.ts
new file mode 100644
index 0000000000000..da102405c179d
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.test.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { formatMillisForDisplay, shouldShowDurationWarning } from './execution_duration_utils';
+import { AlertType as RuleType } from '../../types';
+
+describe('formatMillisForDisplay', () => {
+  it('should return 0 for undefined', () => {
+    expect(formatMillisForDisplay(undefined)).toEqual('00:00:00.000');
+  });
+
+  it('should correctly format millisecond duration in milliseconds', () => {
+    expect(formatMillisForDisplay(845)).toEqual('00:00:00.845');
+  });
+
+  it('should correctly format second duration in milliseconds', () => {
+    expect(formatMillisForDisplay(45200)).toEqual('00:00:45.200');
+  });
+
+  it('should correctly format minute duration in milliseconds', () => {
+    expect(formatMillisForDisplay(122450)).toEqual('00:02:02.450');
+  });
+
+  it('should correctly format hour duration in milliseconds', () => {
+    expect(formatMillisForDisplay(3634601)).toEqual('01:00:34.601');
+  });
+});
+
+describe('shouldShowDurationWarning', () => {
+  it('should return false if rule type or ruleTaskTimeout is undefined', () => {
+    expect(shouldShowDurationWarning(undefined, 1000)).toEqual(false);
+    expect(shouldShowDurationWarning(mockRuleType(), 1000)).toEqual(false);
+  });
+
+  it('should return false if average duration is less than rule task timeout', () => {
+    expect(shouldShowDurationWarning(mockRuleType({ ruleTaskTimeout: '1m' }), 1000)).toEqual(false);
+  });
+
+  it('should return true if average duration is greater than rule task timeout', () => {
+    expect(shouldShowDurationWarning(mockRuleType({ ruleTaskTimeout: '1m' }), 120000)).toEqual(
+      true
+    );
+  });
+});
+
+function mockRuleType(overwrites: Partial<RuleType> = {}): RuleType {
+  return {
+    id: 'test.testRuleType',
+    name: 'My Test Rule Type',
+    actionGroups: [{ id: 'default', name: 'Default Action Group' }],
+    actionVariables: {
+      context: [],
+      state: [],
+      params: [],
+    },
+    defaultActionGroupId: 'default',
+    recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
+    authorizedConsumers: {},
+    producer: 'alerts',
+    minimumLicenseRequired: 'basic',
+    enabledInLicense: true,
+    ...overwrites,
+  };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.ts
new file mode 100644
index 0000000000000..ecad5475274ce
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/execution_duration_utils.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import moment from 'moment';
+import { padStart } from 'lodash';
+import { AlertType as RuleType } from '../../types';
+import { parseDuration } from '../../../../alerting/common';
+
+export function formatMillisForDisplay(value: number | undefined) {
+  if (!value) {
+    return '00:00:00.000';
+  }
+
+  const duration = moment.duration(value);
+  const durationString = [duration.hours(), duration.minutes(), duration.seconds()]
+    .map((v: number) => padStart(`${v}`, 2, '0'))
+    .join(':');
+
+  // add millis
+  const millisString = padStart(`${duration.milliseconds()}`, 3, '0');
+  return `${durationString}.${millisString}`;
+}
+
+export function shouldShowDurationWarning(
+  ruleType: RuleType | undefined,
+  avgDurationMillis: number
+) {
+  if (!ruleType || !ruleType.ruleTaskTimeout) {
+    return false;
+  }
+  const ruleTypeTimeout: string = ruleType.ruleTaskTimeout;
+  const ruleTypeTimeoutMillis: number = parseDuration(ruleTypeTimeout);
+  return avgDurationMillis > ruleTypeTimeoutMillis;
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss
index 76fd94fd4044b..454d6e20d9323 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss
@@ -9,3 +9,8 @@
     width: 85%;
   }
 }
+
+.ruleDurationWarningIcon {
+  bottom: $euiSizeXS;
+  position: relative;
+}
\ No newline at end of file
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx
index cada7748062f1..a790427e36483 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx
@@ -8,9 +8,13 @@
 import * as React from 'react';
 import uuid from 'uuid';
 import { shallow } from 'enzyme';
+import { mountWithIntl, nextTick } from '@kbn/test/jest';
+import { act } from 'react-dom/test-utils';
 import { AlertInstances, AlertInstanceListItem, alertInstanceToListItem } from './alert_instances';
 import { Alert, AlertInstanceSummary, AlertInstanceStatus, AlertType } from '../../../../types';
 import { EuiBasicTable } from '@elastic/eui';
+import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
+
 jest.mock('../../../../common/lib/kibana');
 
 const fakeNow = new Date('2020-02-09T23:15:41.941Z');
@@ -271,6 +275,118 @@ describe('alertInstanceToListItem', () => {
   });
 });
 
+describe('execution duration overview', () => {
+  it('render last execution status', async () => {
+    const rule = mockAlert({
+      executionStatus: { status: 'ok', lastExecutionDate: new Date('2020-08-20T19:23:38Z') },
+    });
+    const ruleType = mockAlertType();
+    const alertSummary = mockAlertInstanceSummary();
+
+    const wrapper = mountWithIntl(
+      <AlertInstances
+        {...mockAPIs}
+        alert={rule}
+        alertType={ruleType}
+        readOnly={false}
+        alertInstanceSummary={alertSummary}
+      />
+    );
+
+    await act(async () => {
+      await nextTick();
+      wrapper.update();
+    });
+
+    const ruleExecutionStatusStat = wrapper.find('[data-test-subj="ruleStatus-ok"]');
+    expect(ruleExecutionStatusStat.exists()).toBeTruthy();
+    expect(ruleExecutionStatusStat.first().prop('description')).toEqual('Last response');
+    expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').text()).toEqual('Ok');
+  });
+
+  it('renders average execution duration', async () => {
+    const rule = mockAlert();
+    const ruleType = mockAlertType({ ruleTaskTimeout: '10m' });
+    const alertSummary = mockAlertInstanceSummary({
+      executionDuration: { average: 60284, values: [] },
+    });
+
+    const wrapper = mountWithIntl(
+      <AlertInstances
+        {...mockAPIs}
+        alert={rule}
+        alertType={ruleType}
+        readOnly={false}
+        alertInstanceSummary={alertSummary}
+      />
+    );
+
+    await act(async () => {
+      await nextTick();
+      wrapper.update();
+    });
+
+    const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
+    expect(avgExecutionDurationPanel.exists()).toBeTruthy();
+    expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
+    expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
+      'Average duration00:01:00.284'
+    );
+    expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
+  });
+
+  it('renders warning when average execution duration exceeds rule timeout', async () => {
+    const rule = mockAlert();
+    const ruleType = mockAlertType({ ruleTaskTimeout: '10m' });
+    const alertSummary = mockAlertInstanceSummary({
+      executionDuration: { average: 60284345, values: [] },
+    });
+
+    const wrapper = mountWithIntl(
+      <AlertInstances
+        {...mockAPIs}
+        alert={rule}
+        alertType={ruleType}
+        readOnly={false}
+        alertInstanceSummary={alertSummary}
+      />
+    );
+
+    await act(async () => {
+      await nextTick();
+      wrapper.update();
+    });
+
+    const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
+    expect(avgExecutionDurationPanel.exists()).toBeTruthy();
+    expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning');
+    expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
+      'Average duration16:44:44.345'
+    );
+    expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy();
+  });
+
+  it('renders execution duration chart', () => {
+    const rule = mockAlert();
+    const ruleType = mockAlertType();
+    const alertSummary = mockAlertInstanceSummary();
+
+    expect(
+      shallow(
+        <AlertInstances
+          {...mockAPIs}
+          alert={rule}
+          alertType={ruleType}
+          alertInstanceSummary={alertSummary}
+          readOnly={false}
+        />
+      )
+        .find(ExecutionDurationChart)
+        .exists()
+    ).toBeTruthy();
+  });
+});
+
 function mockAlert(overloads: Partial<Alert> = {}): Alert {
   return {
     id: uuid.v4(),
@@ -342,6 +458,10 @@ function mockAlertInstanceSummary(
         actionGroupId: 'testActionGroup',
       },
     },
+    executionDuration: {
+      average: 0,
+      values: [],
+    },
   };
   return { ...summary, ...overloads };
 }
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx
index 1583cb188f1c1..9de70699edbdb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx
@@ -8,11 +8,26 @@
 import React, { useState } from 'react';
 import moment, { Duration } from 'moment';
 import { i18n } from '@kbn/i18n';
-import { EuiBasicTable, EuiHealth, EuiSpacer, EuiToolTip } from '@elastic/eui';
+import {
+  EuiBasicTable,
+  EuiHealth,
+  EuiSpacer,
+  EuiToolTip,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiHorizontalRule,
+  EuiPanel,
+  EuiStat,
+  EuiIconTip,
+} from '@elastic/eui';
 // @ts-ignore
 import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services';
 import { padStart, chunk } from 'lodash';
-import { ActionGroup, AlertInstanceStatusValues } from '../../../../../../alerting/common';
+import {
+  ActionGroup,
+  AlertExecutionStatusErrorReasons,
+  AlertInstanceStatusValues,
+} from '../../../../../../alerting/common';
 import {
   Alert,
   AlertInstanceSummary,
@@ -27,6 +42,16 @@ import {
 import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
 import './alert_instances.scss';
 import { RuleMutedSwitch } from './rule_muted_switch';
+import { getHealthColor } from '../../alerts_list/components/alert_status_filter';
+import {
+  alertsStatusesTranslationsMapping,
+  ALERT_STATUS_LICENSE_ERROR,
+} from '../../alerts_list/translations';
+import {
+  formatMillisForDisplay,
+  shouldShowDurationWarning,
+} from '../../../lib/execution_duration_utils';
+import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
 
 type AlertInstancesProps = {
   alert: Alert;
@@ -161,8 +186,91 @@ export function AlertInstances({
     requestRefresh();
   };
 
+  const showDurationWarning = shouldShowDurationWarning(
+    alertType,
+    alertInstanceSummary.executionDuration.average
+  );
+
+  const healthColor = getHealthColor(alert.executionStatus.status);
+  const isLicenseError =
+    alert.executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License;
+  const statusMessage = isLicenseError
+    ? ALERT_STATUS_LICENSE_ERROR
+    : alertsStatusesTranslationsMapping[alert.executionStatus.status];
+
   return (
     <>
+      <EuiHorizontalRule />
+      <EuiFlexGroup>
+        <EuiFlexItem grow={1}>
+          <EuiPanel color="subdued" hasBorder={false}>
+            <EuiStat
+              data-test-subj={`ruleStatus-${alert.executionStatus.status}`}
+              titleSize="xs"
+              title={
+                <EuiHealth
+                  data-test-subj={`ruleStatus-${alert.executionStatus.status}`}
+                  textSize="inherit"
+                  color={healthColor}
+                >
+                  {statusMessage}
+                </EuiHealth>
+              }
+              description={i18n.translate(
+                'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.ruleLastExecutionDescription',
+                {
+                  defaultMessage: `Last response`,
+                }
+              )}
+            />
+          </EuiPanel>
+        </EuiFlexItem>
+        <EuiFlexItem grow={1}>
+          <EuiPanel
+            data-test-subj="avgExecutionDurationPanel"
+            color={showDurationWarning ? 'warning' : 'subdued'}
+            hasBorder={false}
+          >
+            <EuiStat
+              data-test-subj="avgExecutionDurationStat"
+              titleSize="xs"
+              title={
+                <EuiFlexGroup gutterSize="xs">
+                  {showDurationWarning && (
+                    <EuiFlexItem grow={false}>
+                      <EuiIconTip
+                        data-test-subj="ruleDurationWarning"
+                        anchorClassName="ruleDurationWarningIcon"
+                        type="alert"
+                        color="warning"
+                        content={i18n.translate(
+                          'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.ruleTypeExcessDurationMessage',
+                          {
+                            defaultMessage: `Duration exceeds the rule's expected run time.`,
+                          }
+                        )}
+                        position="top"
+                      />
+                    </EuiFlexItem>
+                  )}
+                  <EuiFlexItem grow={false}>
+                    {formatMillisForDisplay(alertInstanceSummary.executionDuration.average)}
+                  </EuiFlexItem>
+                </EuiFlexGroup>
+              }
+              description={i18n.translate(
+                'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.avgDurationDescription',
+                {
+                  defaultMessage: `Average duration`,
+                }
+              )}
+            />
+          </EuiPanel>
+        </EuiFlexItem>
+        <EuiFlexItem grow={4}>
+          <ExecutionDurationChart executionDuration={alertInstanceSummary.executionDuration} />
+        </EuiFlexItem>
+      </EuiFlexGroup>
       <EuiSpacer size="xl" />
       <input
         type="hidden"
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx
index d673c30b997cb..a0eb3212f7742 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx
@@ -179,6 +179,10 @@ function mockAlertInstanceSummary(overloads: Partial<any> = {}): any {
         muted: false,
       },
     },
+    executionDuration: {
+      average: 0,
+      values: [],
+    },
   };
   return summary;
 }
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
index 91b1b14083ed2..a7d54a896d7ef 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
@@ -8,7 +8,7 @@
 /* eslint-disable react-hooks/exhaustive-deps */
 
 import { i18n } from '@kbn/i18n';
-import { capitalize, padStart, sortBy } from 'lodash';
+import { capitalize, sortBy } from 'lodash';
 import moment from 'moment';
 import { FormattedMessage } from '@kbn/i18n/react';
 import React, { useEffect, useState } from 'react';
@@ -72,7 +72,6 @@ import {
   ALERTS_FEATURE_ID,
   AlertExecutionStatusErrorReasons,
   formatDuration,
-  parseDuration,
 } from '../../../../../../alerting/common';
 import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations';
 import { useKibana } from '../../../../common/lib/kibana';
@@ -82,6 +81,10 @@ import { CenterJustifiedSpinner } from '../../../components/center_justified_spi
 import { ManageLicenseModal } from './manage_license_modal';
 import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled';
 import { RuleEnabledSwitch } from './rule_enabled_switch';
+import {
+  formatMillisForDisplay,
+  shouldShowDurationWarning,
+} from '../../../lib/execution_duration_utils';
 
 const ENTER_KEY = 13;
 
@@ -523,25 +526,14 @@ export const AlertsList: React.FunctionComponent = () => {
       truncateText: false,
       'data-test-subj': 'alertsTableCell-duration',
       render: (value: number, item: AlertTableItem) => {
-        const ruleTypeTimeout: string | undefined = alertTypesState.data.get(
-          item.alertTypeId
-        )?.ruleTaskTimeout;
-        const ruleTypeTimeoutMillis: number | undefined = ruleTypeTimeout
-          ? parseDuration(ruleTypeTimeout)
-          : undefined;
-        const showDurationWarning: boolean = ruleTypeTimeoutMillis
-          ? value > ruleTypeTimeoutMillis
-          : false;
-        const duration = moment.duration(value);
-        const durationString = [duration.hours(), duration.minutes(), duration.seconds()]
-          .map((v: number) => padStart(`${v}`, 2, '0'))
-          .join(':');
+        const showDurationWarning = shouldShowDurationWarning(
+          alertTypesState.data.get(item.alertTypeId),
+          value
+        );
 
-        // add millis
-        const millisString = padStart(`${duration.milliseconds()}`, 3, '0');
         return (
           <>
-            {`${durationString}.${millisString}`}
+            {`${formatMillisForDisplay(value)}`}
             {showDurationWarning && (
               <EuiIconTip
                 data-test-subj="ruleDurationWarning"
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx
new file mode 100644
index 0000000000000..672cd75f0cd13
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as React from 'react';
+import { mountWithIntl, nextTick } from '@kbn/test/jest';
+import { act } from 'react-dom/test-utils';
+import { ExecutionDurationChart, padOrTruncateDurations } from './execution_duration_chart';
+
+describe('execution duration chart', () => {
+  it('renders empty state when no execution duration values are available', async () => {
+    const executionDuration = mockExecutionDuration();
+
+    const wrapper = mountWithIntl(<ExecutionDurationChart executionDuration={executionDuration} />);
+
+    await act(async () => {
+      await nextTick();
+      wrapper.update();
+    });
+
+    expect(wrapper.find('[data-test-subj="executionDurationChartPanel"]').exists()).toBeTruthy();
+    expect(wrapper.find('[data-test-subj="executionDurationChartEmpty"]').exists()).toBeTruthy();
+    expect(wrapper.find('[data-test-subj="executionDurationChart"]').exists()).toBeFalsy();
+  });
+
+  it('renders chart when execution duration values are available', async () => {
+    const executionDuration = mockExecutionDuration({ average: 10, values: [1, 2] });
+
+    const wrapper = mountWithIntl(<ExecutionDurationChart executionDuration={executionDuration} />);
+
+    await act(async () => {
+      await nextTick();
+      wrapper.update();
+    });
+
+    expect(wrapper.find('[data-test-subj="executionDurationChartPanel"]').exists()).toBeTruthy();
+    expect(wrapper.find('[data-test-subj="executionDurationChartEmpty"]').exists()).toBeFalsy();
+    expect(wrapper.find('[data-test-subj="executionDurationChart"]').exists()).toBeTruthy();
+  });
+});
+
+describe('padOrTruncateDurations', () => {
+  it('does nothing when array is the correct length', () => {
+    expect(padOrTruncateDurations([1, 2, 3], 3)).toEqual([1, 2, 3]);
+  });
+
+  it('pads execution duration values when there are fewer than display desires', () => {
+    expect(padOrTruncateDurations([1, 2, 3], 10)).toEqual([1, 2, 3, 0, 0, 0, 0, 0, 0, 0]);
+  });
+
+  it('truncates execution duration values when there are more than display desires', () => {
+    expect(padOrTruncateDurations([1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13], 10)).toEqual([
+      3, 5, 6, 7, 8, 9, 10, 11, 12, 13,
+    ]);
+  });
+});
+
+function mockExecutionDuration(
+  overwrites: {
+    average?: number;
+    values?: number[];
+  } = {}
+) {
+  return {
+    average: 0,
+    values: [],
+    ...overwrites,
+  };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx
new file mode 100644
index 0000000000000..ca20af9cea0fd
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiPanel,
+  EuiEmptyPrompt,
+  EuiIconTip,
+  EuiTitle,
+} from '@elastic/eui';
+import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json';
+import { Axis, BarSeries, Chart, CurveType, LineSeries, Settings } from '@elastic/charts';
+import { assign, fill } from 'lodash';
+import { formatMillisForDisplay } from '../../../lib/execution_duration_utils';
+
+export interface ComponentOpts {
+  executionDuration: {
+    average: number;
+    values: number[];
+  };
+}
+
+const DESIRED_NUM_EXECUTION_DURATIONS = 30;
+
+export const ExecutionDurationChart: React.FunctionComponent<ComponentOpts> = ({
+  executionDuration,
+}: ComponentOpts) => {
+  const paddedExecutionDurations = padOrTruncateDurations(
+    executionDuration.values,
+    DESIRED_NUM_EXECUTION_DURATIONS
+  );
+
+  return (
+    <EuiPanel data-test-subj="executionDurationChartPanel" hasBorder={true}>
+      <EuiFlexGroup alignItems="center" gutterSize="xs">
+        <EuiFlexItem grow={false}>
+          <EuiTitle size="xxs">
+            <h4>
+              <FormattedMessage
+                id="xpack.triggersActionsUI.sections.executionDurationChart.recentDurationsTitle"
+                defaultMessage="Recent execution durations"
+              />
+            </h4>
+          </EuiTitle>
+        </EuiFlexItem>
+        <EuiFlexItem grow={false}>
+          <EuiIconTip
+            color="subdued"
+            type="questionInCircle"
+            content={i18n.translate(
+              'xpack.triggersActionsUI.sections.executionDurationChart.recentDurationsTooltip',
+              {
+                defaultMessage: `Recent rule executions include up to the last {numExecutions} executions.`,
+                values: {
+                  numExecutions: DESIRED_NUM_EXECUTION_DURATIONS,
+                },
+              }
+            )}
+            position="top"
+          />
+        </EuiFlexItem>
+      </EuiFlexGroup>
+
+      {executionDuration.values && executionDuration.values.length > 0 ? (
+        <>
+          <Chart data-test-subj="executionDurationChart" size={{ height: 80 }}>
+            <Settings
+              theme={{
+                lineSeriesStyle: {
+                  point: { visible: false },
+                  line: { stroke: lightEuiTheme.euiColorAccent },
+                },
+              }}
+            />
+            <BarSeries
+              id="executionDuration"
+              xScaleType="linear"
+              yScaleType="linear"
+              xAccessor={0}
+              yAccessors={[1]}
+              data={paddedExecutionDurations.map((val, ndx) => [ndx, val])}
+            />
+            <LineSeries
+              id="rule_duration_avg"
+              xScaleType="linear"
+              yScaleType="linear"
+              xAccessor={0}
+              yAccessors={[1]}
+              data={paddedExecutionDurations.map((val, ndx) => [ndx, executionDuration.average])}
+              curve={CurveType.CURVE_NATURAL}
+            />
+            <Axis id="left-axis" position="left" tickFormat={(d) => formatMillisForDisplay(d)} />
+          </Chart>
+        </>
+      ) : (
+        <>
+          <EuiEmptyPrompt
+            data-test-subj="executionDurationChartEmpty"
+            body={
+              <>
+                <p>
+                  <FormattedMessage
+                    id="xpack.triggersActionsUI.sections.executionDurationChart.executionDurationNoData"
+                    defaultMessage="There are no available executions for this rule."
+                  />
+                </p>
+              </>
+            }
+          />
+        </>
+      )}
+    </EuiPanel>
+  );
+};
+
+export function padOrTruncateDurations(values: number[], desiredSize: number) {
+  if (values.length === desiredSize) {
+    return values;
+  } else if (values.length < desiredSize) {
+    return assign(fill(new Array(desiredSize), 0), values);
+  } else {
+    // oldest durations are at the start of the array, so take the last {desiredSize} values
+    return values.slice(-desiredSize);
+  }
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts
index d4ca6c2aa9cd8..181e6cc940c38 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts
@@ -73,6 +73,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr
                 'status_start_date',
                 'status_end_date',
                 'last_run',
+                'execution_duration',
               ]);
               expect(stableBody).to.eql({
                 name: 'abc',
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts
index 099502e375faa..ff55c376c04b2 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts
@@ -58,7 +58,12 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr
       const { status_start_date: statusStartDate, status_end_date: statusEndDate } = response.body;
       expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate));
 
-      const stableBody = omit(response.body, ['status_start_date', 'status_end_date', 'last_run']);
+      const stableBody = omit(response.body, [
+        'status_start_date',
+        'status_end_date',
+        'last_run',
+        'execution_duration',
+      ]);
       expect(stableBody).to.eql({
         id: createdAlert.id,
         name: 'abc',
@@ -91,7 +96,12 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr
       const { status_start_date: statusStartDate, status_end_date: statusEndDate } = response.body;
       expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate));
 
-      const stableBody = omit(response.body, ['status_start_date', 'status_end_date', 'last_run']);
+      const stableBody = omit(response.body, [
+        'status_start_date',
+        'status_end_date',
+        'last_run',
+        'execution_duration',
+      ]);
       expect(stableBody).to.eql({
         id: createdAlert.id,
         name: 'abc',