feat: Add External Status Page Monitor functionality

- Introduced External Status Page Monitor to monitor third-party status pages.
- Implemented fetching logic for Atlassian Statuspage, RSS, and Atom feeds.
- Added new types and interfaces for handling external status page responses.
- Created UI components for configuring and displaying external status page monitors.
- Updated documentation to include details on the new monitor type and its configuration options.
This commit is contained in:
Nawaz Dhandala
2026-02-23 19:20:04 +00:00
parent 83149665e8
commit 14cd9d249f
24 changed files with 1554 additions and 8 deletions

View File

@@ -135,6 +135,15 @@ export class Service extends DatabaseService<Model> {
monitorDestination = `${monitorDestination} @${firstStep.data.dnsMonitor.hostname}`;
}
}
// For External Status Page monitors, use the statusPageUrl
if (
monitorType === MonitorType.ExternalStatusPage &&
firstStep?.data?.externalStatusPageMonitor
) {
monitorDestination =
firstStep.data.externalStatusPageMonitor.statusPageUrl || "";
}
}
}

View File

@@ -0,0 +1,163 @@
import DataToProcess from "../DataToProcess";
import CompareCriteria from "./CompareCriteria";
import {
CheckOn,
CriteriaFilter,
FilterType,
} from "../../../../Types/Monitor/CriteriaFilter";
import ExternalStatusPageMonitorResponse from "../../../../Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
import EvaluateOverTime from "./EvaluateOverTime";
import CaptureSpan from "../../Telemetry/CaptureSpan";
import logger from "../../Logger";
export default class ExternalStatusPageMonitorCriteria {
@CaptureSpan()
public static async isMonitorInstanceCriteriaFilterMet(input: {
dataToProcess: DataToProcess;
criteriaFilter: CriteriaFilter;
}): Promise<string | null> {
let threshold: number | string | undefined | null =
input.criteriaFilter.value;
const dataToProcess: ProbeMonitorResponse =
input.dataToProcess as ProbeMonitorResponse;
const externalStatusPageResponse:
| ExternalStatusPageMonitorResponse
| undefined = dataToProcess.externalStatusPageResponse;
let overTimeValue: Array<number | boolean> | number | boolean | undefined =
undefined;
if (
input.criteriaFilter.evaluateOverTime &&
input.criteriaFilter.evaluateOverTimeOptions
) {
try {
overTimeValue = await EvaluateOverTime.getValueOverTime({
projectId: (input.dataToProcess as ProbeMonitorResponse).projectId,
monitorId: input.dataToProcess.monitorId!,
evaluateOverTimeOptions: input.criteriaFilter.evaluateOverTimeOptions,
metricType: input.criteriaFilter.checkOn,
});
if (Array.isArray(overTimeValue) && overTimeValue.length === 0) {
overTimeValue = undefined;
}
} catch (err) {
logger.error(
`Error in getting over time value for ${input.criteriaFilter.checkOn}`,
);
logger.error(err);
overTimeValue = undefined;
}
}
// Check if external status page is online
if (
input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageIsOnline
) {
const currentIsOnline: boolean | Array<boolean> =
(overTimeValue as Array<boolean>) ||
(input.dataToProcess as ProbeMonitorResponse).isOnline;
return CompareCriteria.compareCriteriaBoolean({
value: currentIsOnline,
criteriaFilter: input.criteriaFilter,
});
}
// Check external status page response time
if (
input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageResponseTime
) {
threshold = CompareCriteria.convertToNumber(threshold);
if (threshold === null || threshold === undefined) {
return null;
}
const currentResponseTime: number | Array<number> =
(overTimeValue as Array<number>) ||
externalStatusPageResponse?.responseTimeInMs ||
(input.dataToProcess as ProbeMonitorResponse).responseTimeInMs;
if (currentResponseTime === null || currentResponseTime === undefined) {
return null;
}
return CompareCriteria.compareCriteriaNumbers({
value: currentResponseTime,
threshold: threshold as number,
criteriaFilter: input.criteriaFilter,
});
}
// Check overall status
if (
input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageOverallStatus
) {
if (!externalStatusPageResponse?.overallStatus) {
return null;
}
return CompareCriteria.compareCriteriaStrings({
value: externalStatusPageResponse.overallStatus,
threshold: String(threshold),
criteriaFilter: input.criteriaFilter,
});
}
// Check component status
if (
input.criteriaFilter.checkOn ===
CheckOn.ExternalStatusPageComponentStatus
) {
if (
!externalStatusPageResponse?.componentStatuses ||
externalStatusPageResponse.componentStatuses.length === 0
) {
return null;
}
// Check if any component status matches the criteria
for (const component of externalStatusPageResponse.componentStatuses) {
const result: string | null = CompareCriteria.compareCriteriaStrings({
value: component.status,
threshold: String(threshold),
criteriaFilter: input.criteriaFilter,
});
if (result) {
return `Component "${component.name}": ${result}`;
}
}
return null;
}
// Check active incidents count
if (
input.criteriaFilter.checkOn ===
CheckOn.ExternalStatusPageActiveIncidents
) {
threshold = CompareCriteria.convertToNumber(threshold);
if (threshold === null || threshold === undefined) {
return null;
}
const activeIncidents: number =
externalStatusPageResponse?.activeIncidentCount || 0;
return CompareCriteria.compareCriteriaNumbers({
value: activeIncidents,
threshold: threshold as number,
criteriaFilter: input.criteriaFilter,
});
}
return null;
}
}

View File

@@ -14,6 +14,7 @@ import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
import SnmpMonitorCriteria from "./Criteria/SnmpMonitorCriteria";
import DnsMonitorCriteria from "./Criteria/DnsMonitorCriteria";
import DomainMonitorCriteria from "./Criteria/DomainMonitorCriteria";
import ExternalStatusPageMonitorCriteria from "./Criteria/ExternalStatusPageMonitorCriteria";
import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
@@ -519,6 +520,20 @@ ${contextBlock}
}
}
if (input.monitor.monitorType === MonitorType.ExternalStatusPage) {
const externalStatusPageResult: string | null =
await ExternalStatusPageMonitorCriteria.isMonitorInstanceCriteriaFilterMet(
{
dataToProcess: input.dataToProcess,
criteriaFilter: input.criteriaFilter,
},
);
if (externalStatusPageResult) {
return externalStatusPageResult;
}
}
return null;
}

View File

@@ -18,6 +18,9 @@ import DnsMonitorResponse, {
DnsRecordResponse,
} from "../../../Types/Monitor/DnsMonitor/DnsMonitorResponse";
import DomainMonitorResponse from "../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
import ExternalStatusPageMonitorResponse, {
ExternalStatusPageComponentStatus,
} from "../../../Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
import Typeof from "../../../Types/Typeof";
import VMUtil from "../VM/VMAPI";
import DataToProcess from "./DataToProcess";
@@ -298,6 +301,36 @@ export default class MonitorTemplateUtil {
dnssec: domainResponse?.dnssec,
} as JSONObject;
}
if (data.monitorType === MonitorType.ExternalStatusPage) {
const externalStatusPageResponse:
| ExternalStatusPageMonitorResponse
| undefined = (data.dataToProcess as ProbeMonitorResponse)
.externalStatusPageResponse;
storageMap = {
isOnline: (data.dataToProcess as ProbeMonitorResponse).isOnline,
responseTimeInMs: externalStatusPageResponse?.responseTimeInMs,
failureCause: externalStatusPageResponse?.failureCause,
overallStatus: externalStatusPageResponse?.overallStatus,
activeIncidentCount:
externalStatusPageResponse?.activeIncidentCount,
} as JSONObject;
// Add component statuses
if (externalStatusPageResponse?.componentStatuses) {
storageMap["componentStatuses"] =
externalStatusPageResponse.componentStatuses.map(
(component: ExternalStatusPageComponentStatus) => {
return {
name: component.name,
status: component.status,
description: component.description,
};
},
);
}
}
} catch (err) {
logger.error(err);
}

View File

@@ -74,6 +74,13 @@ export enum CheckOn {
DomainNameServer = "Domain Name Server",
DomainStatusCode = "Domain Status Code",
DomainIsExpired = "Domain Is Expired",
// External Status Page monitors.
ExternalStatusPageIsOnline = "External Status Page Is Online",
ExternalStatusPageOverallStatus = "External Status Page Overall Status",
ExternalStatusPageComponentStatus = "External Status Page Component Status",
ExternalStatusPageActiveIncidents = "External Status Page Active Incidents",
ExternalStatusPageResponseTime = "External Status Page Response Time (in ms)",
}
export interface ServerMonitorOptions {
@@ -159,7 +166,8 @@ export class CriteriaFilterUtil {
checkOn === CheckOn.IsOnline ||
checkOn === CheckOn.SnmpIsOnline ||
checkOn === CheckOn.DnsIsOnline ||
checkOn === CheckOn.DomainIsExpired
checkOn === CheckOn.DomainIsExpired ||
checkOn === CheckOn.ExternalStatusPageIsOnline
) {
return false;
}
@@ -229,7 +237,9 @@ export class CriteriaFilterUtil {
checkOn === CheckOn.SnmpResponseTime ||
checkOn === CheckOn.SnmpIsOnline ||
checkOn === CheckOn.DnsResponseTime ||
checkOn === CheckOn.DnsIsOnline
checkOn === CheckOn.DnsIsOnline ||
checkOn === CheckOn.ExternalStatusPageResponseTime ||
checkOn === CheckOn.ExternalStatusPageIsOnline
);
}
}

View File

@@ -0,0 +1,16 @@
export interface ExternalStatusPageComponentStatus {
name: string;
status: string;
description?: string | undefined;
}
export default interface ExternalStatusPageMonitorResponse {
isOnline: boolean;
overallStatus: string;
componentStatuses: Array<ExternalStatusPageComponentStatus>;
activeIncidentCount: number;
responseTimeInMs: number;
failureCause: string;
rawBody?: string | undefined;
isTimeout?: boolean | undefined;
}

View File

@@ -0,0 +1,8 @@
enum ExternalStatusPageProviderType {
AtlassianStatuspage = "Atlassian Statuspage",
RSS = "RSS",
Atom = "Atom",
Auto = "Auto",
}
export default ExternalStatusPageProviderType;

View File

@@ -448,10 +448,35 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
return monitorCriteriaInstance;
}
return null;
}
if (arg.monitorType === MonitorType.ExternalStatusPage) {
const monitorCriteriaInstance: MonitorCriteriaInstance =
new MonitorCriteriaInstance();
public static getDefaultOfflineMonitorCriteriaInstance(arg: {
monitorCriteriaInstance.data = {
id: ObjectID.generate().toString(),
monitorStatusId: arg.monitorStatusId,
filterCondition: FilterCondition.All,
filters: [
{
checkOn: CheckOn.ExternalStatusPageIsOnline,
filterType: FilterType.True,
value: undefined,
},
],
incidents: [],
alerts: [],
createAlerts: false,
changeMonitorStatus: true,
createIncidents: false,
name: `Check if ${arg.monitorName} is online`,
description: `This criteria checks if the ${arg.monitorName} external status page is reachable`,
};
return monitorCriteriaInstance;
}
return null;
}(arg: {
monitorType: MonitorType;
monitorStatusId: ObjectID;
incidentSeverityId: ObjectID;
@@ -629,9 +654,48 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
};
}
if (arg.monitorType === MonitorType.ExternalStatusPage) {
monitorCriteriaInstance.data = {
id: ObjectID.generate().toString(),
monitorStatusId: arg.monitorStatusId,
filterCondition: FilterCondition.Any,
filters: [
{
checkOn: CheckOn.ExternalStatusPageIsOnline,
filterType: FilterType.False,
value: undefined,
},
],
incidents: [
{
title: `${arg.monitorName} is offline`,
description: `${arg.monitorName} external status page is currently unreachable.`,
incidentSeverityId: arg.incidentSeverityId,
autoResolveIncident: true,
id: ObjectID.generate().toString(),
onCallPolicyIds: [],
},
],
changeMonitorStatus: true,
createIncidents: true,
createAlerts: false,
alerts: [
{
title: `${arg.monitorName} is offline`,
description: `${arg.monitorName} external status page is currently unreachable.`,
alertSeverityId: arg.alertSeverityId,
autoResolveAlert: true,
id: ObjectID.generate().toString(),
onCallPolicyIds: [],
},
],
name: `Check if ${arg.monitorName} is offline`,
description: `This criteria checks if the ${arg.monitorName} external status page is unreachable`,
};
}
if (
arg.monitorType === MonitorType.API ||
arg.monitorType === MonitorType.Website
) {
monitorCriteriaInstance.data = {
id: ObjectID.generate().toString(),

View File

@@ -35,6 +35,9 @@ import MonitorStepDnsMonitor, {
import MonitorStepDomainMonitor, {
MonitorStepDomainMonitorUtil,
} from "./MonitorStepDomainMonitor";
import MonitorStepExternalStatusPageMonitor, {
MonitorStepExternalStatusPageMonitorUtil,
} from "./MonitorStepExternalStatusPageMonitor";
import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
export interface MonitorStepType {
@@ -84,6 +87,9 @@ export interface MonitorStepType {
// Domain monitor
domainMonitor?: MonitorStepDomainMonitor | undefined;
// External Status Page monitor
externalStatusPageMonitor?: MonitorStepExternalStatusPageMonitor | undefined;
}
export default class MonitorStep extends DatabaseProperty {
@@ -112,6 +118,7 @@ export default class MonitorStep extends DatabaseProperty {
snmpMonitor: undefined,
dnsMonitor: undefined,
domainMonitor: undefined,
externalStatusPageMonitor: undefined,
};
}
@@ -145,6 +152,7 @@ export default class MonitorStep extends DatabaseProperty {
snmpMonitor: undefined,
dnsMonitor: undefined,
domainMonitor: undefined,
externalStatusPageMonitor: undefined,
};
return monitorStep;
@@ -252,6 +260,13 @@ export default class MonitorStep extends DatabaseProperty {
return this;
}
public setExternalStatusPageMonitor(
externalStatusPageMonitor: MonitorStepExternalStatusPageMonitor,
): MonitorStep {
this.data!.externalStatusPageMonitor = externalStatusPageMonitor;
return this;
}
public setCustomCode(customCode: string): MonitorStep {
this.data!.customCode = customCode;
return this;
@@ -380,6 +395,16 @@ export default class MonitorStep extends DatabaseProperty {
}
}
if (monitorType === MonitorType.ExternalStatusPage) {
if (!value.data.externalStatusPageMonitor) {
return "External status page configuration is required";
}
if (!value.data.externalStatusPageMonitor.statusPageUrl) {
return "Status page URL is required";
}
}
return null;
}
@@ -431,6 +456,11 @@ export default class MonitorStep extends DatabaseProperty {
domainMonitor: this.data.domainMonitor
? MonitorStepDomainMonitorUtil.toJSON(this.data.domainMonitor)
: undefined,
externalStatusPageMonitor: this.data.externalStatusPageMonitor
? MonitorStepExternalStatusPageMonitorUtil.toJSON(
this.data.externalStatusPageMonitor,
)
: undefined,
},
});
}
@@ -542,6 +572,9 @@ export default class MonitorStep extends DatabaseProperty {
domainMonitor: json["domainMonitor"]
? (json["domainMonitor"] as JSONObject)
: undefined,
externalStatusPageMonitor: json["externalStatusPageMonitor"]
? (json["externalStatusPageMonitor"] as JSONObject)
: undefined,
}) as any;
return monitorStep;
@@ -569,6 +602,7 @@ export default class MonitorStep extends DatabaseProperty {
snmpMonitor: Zod.any().optional(),
dnsMonitor: Zod.any().optional(),
domainMonitor: Zod.any().optional(),
externalStatusPageMonitor: Zod.any().optional(),
}).openapi({
type: "object",
example: {

View File

@@ -0,0 +1,48 @@
import { JSONObject } from "../JSON";
import ExternalStatusPageProviderType from "./ExternalStatusPageProviderType";
export default interface MonitorStepExternalStatusPageMonitor {
statusPageUrl: string;
provider: ExternalStatusPageProviderType;
componentName?: string | undefined; // optional: filter to a specific component
timeout: number;
retries: number;
}
export class MonitorStepExternalStatusPageMonitorUtil {
public static getDefault(): MonitorStepExternalStatusPageMonitor {
return {
statusPageUrl: "",
provider: ExternalStatusPageProviderType.Auto,
componentName: undefined,
timeout: 10000,
retries: 3,
};
}
public static fromJSON(
json: JSONObject,
): MonitorStepExternalStatusPageMonitor {
return {
statusPageUrl: (json["statusPageUrl"] as string) || "",
provider:
(json["provider"] as ExternalStatusPageProviderType) ||
ExternalStatusPageProviderType.Auto,
componentName: (json["componentName"] as string) || undefined,
timeout: (json["timeout"] as number) || 10000,
retries: (json["retries"] as number) || 3,
};
}
public static toJSON(
monitor: MonitorStepExternalStatusPageMonitor,
): JSONObject {
return {
statusPageUrl: monitor.statusPageUrl,
provider: monitor.provider,
componentName: monitor.componentName,
timeout: monitor.timeout,
retries: monitor.retries,
};
}
}

View File

@@ -32,6 +32,9 @@ enum MonitorType {
// Domain registration monitoring
Domain = "Domain",
// External status page monitoring
ExternalStatusPage = "External Status Page",
}
export default MonitorType;
@@ -62,6 +65,7 @@ export class MonitorTypeHelper {
MonitorType.DNS,
MonitorType.SSLCertificate,
MonitorType.Domain,
MonitorType.ExternalStatusPage,
],
},
{
@@ -250,6 +254,13 @@ export class MonitorTypeHelper {
"This monitor type lets you monitor domain registration health — expiry dates, registrar info, nameserver delegation, and WHOIS status.",
icon: IconProp.Globe,
},
{
monitorType: MonitorType.ExternalStatusPage,
title: "External Status Page",
description:
"This monitor type lets you monitor third-party status pages (e.g. AWS, GCP, Azure, GitHub, Cloudflare) and alert when their services degrade.",
icon: IconProp.ExternalLink,
},
];
return monitorTypeProps;
@@ -297,7 +308,8 @@ export class MonitorTypeHelper {
monitorType === MonitorType.CustomJavaScriptCode ||
monitorType === MonitorType.SNMP ||
monitorType === MonitorType.DNS ||
monitorType === MonitorType.Domain;
monitorType === MonitorType.Domain ||
monitorType === MonitorType.ExternalStatusPage;
return isProbeableMonitor;
}
@@ -321,6 +333,7 @@ export class MonitorTypeHelper {
MonitorType.SNMP,
MonitorType.DNS,
MonitorType.Domain,
MonitorType.ExternalStatusPage,
];
}
@@ -355,7 +368,8 @@ export class MonitorTypeHelper {
monitorType === MonitorType.CustomJavaScriptCode ||
monitorType === MonitorType.SNMP ||
monitorType === MonitorType.DNS ||
monitorType === MonitorType.Domain
monitorType === MonitorType.Domain ||
monitorType === MonitorType.ExternalStatusPage
) {
return true;
}

View File

@@ -9,6 +9,7 @@ import SyntheticMonitorResponse from "../Monitor/SyntheticMonitors/SyntheticMoni
import SnmpMonitorResponse from "../Monitor/SnmpMonitor/SnmpMonitorResponse";
import DnsMonitorResponse from "../Monitor/DnsMonitor/DnsMonitorResponse";
import DomainMonitorResponse from "../Monitor/DomainMonitor/DomainMonitorResponse";
import ExternalStatusPageMonitorResponse from "../Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
import MonitorEvaluationSummary from "../Monitor/MonitorEvaluationSummary";
import ObjectID from "../ObjectID";
import Port from "../Port";
@@ -34,6 +35,7 @@ export default interface ProbeMonitorResponse {
snmpResponse?: SnmpMonitorResponse | undefined;
dnsResponse?: DnsMonitorResponse | undefined;
domainResponse?: DomainMonitorResponse | undefined;
externalStatusPageResponse?: ExternalStatusPageMonitorResponse | undefined;
monitoredAt: Date;
isTimeout?: boolean | undefined;
ingestedAt?: Date | undefined;

View File

@@ -0,0 +1,157 @@
import React, { FunctionComponent, ReactElement, useState } from "react";
import MonitorStepExternalStatusPageMonitor from "Common/Types/Monitor/MonitorStepExternalStatusPageMonitor";
import ExternalStatusPageProviderType from "Common/Types/Monitor/ExternalStatusPageProviderType";
import Input, { InputType } from "Common/UI/Components/Input/Input";
import Dropdown, {
DropdownOption,
DropdownValue,
} from "Common/UI/Components/Dropdown/Dropdown";
import FieldLabelElement from "Common/UI/Components/Forms/Fields/FieldLabel";
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
import DropdownUtil from "Common/UI/Utils/Dropdown";
export interface ComponentProps {
monitorStepExternalStatusPageMonitor: MonitorStepExternalStatusPageMonitor;
onChange: (value: MonitorStepExternalStatusPageMonitor) => void;
}
const ExternalStatusPageMonitorStepForm: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [showAdvancedOptions, setShowAdvancedOptions] =
useState<boolean>(false);
const providerOptions: Array<DropdownOption> =
DropdownUtil.getDropdownOptionsFromEnum(ExternalStatusPageProviderType);
return (
<div className="space-y-5">
<div>
<FieldLabelElement
title="Status Page URL"
description="The URL of the external status page to monitor (e.g. https://www.githubstatus.com)"
required={true}
/>
<Input
initialValue={
props.monitorStepExternalStatusPageMonitor.statusPageUrl
}
placeholder="https://status.example.com"
onChange={(value: string) => {
props.onChange({
...props.monitorStepExternalStatusPageMonitor,
statusPageUrl: value,
});
}}
/>
</div>
<div>
<FieldLabelElement
title="Provider Type"
description="How to fetch status data. Auto will try Atlassian Statuspage JSON API first, then fall back to RSS/Atom."
required={true}
/>
<Dropdown
options={providerOptions}
initialValue={providerOptions.find((option: DropdownOption) => {
return (
option.value ===
props.monitorStepExternalStatusPageMonitor.provider
);
})}
onChange={(value: DropdownValue | Array<DropdownValue> | null) => {
props.onChange({
...props.monitorStepExternalStatusPageMonitor,
provider: value as ExternalStatusPageProviderType,
});
}}
/>
</div>
<div>
<FieldLabelElement
title="Component Name Filter (Optional)"
description="Filter to a specific component by name. Leave blank to monitor overall status."
required={false}
/>
<Input
initialValue={
props.monitorStepExternalStatusPageMonitor.componentName || ""
}
placeholder="e.g. API, Compute Engine, us-east-1"
onChange={(value: string) => {
props.onChange({
...props.monitorStepExternalStatusPageMonitor,
componentName: value || undefined,
});
}}
/>
</div>
{!showAdvancedOptions && (
<div className="mt-1 -ml-3">
<Button
title="Advanced: Timeout and Retries"
buttonStyle={ButtonStyleType.SECONDARY_LINK}
onClick={() => {
setShowAdvancedOptions(true);
}}
/>
</div>
)}
{showAdvancedOptions && (
<div className="space-y-4 border p-4 rounded-md bg-gray-50">
<h4 className="font-medium">Advanced Options</h4>
<div>
<FieldLabelElement
title="Timeout (ms)"
description="How long to wait for a response before timing out"
required={false}
/>
<Input
initialValue={
props.monitorStepExternalStatusPageMonitor.timeout?.toString() ||
"10000"
}
placeholder="10000"
type={InputType.NUMBER}
onChange={(value: string) => {
props.onChange({
...props.monitorStepExternalStatusPageMonitor,
timeout: parseInt(value) || 10000,
});
}}
/>
</div>
<div>
<FieldLabelElement
title="Retries"
description="Number of times to retry on failure"
required={false}
/>
<Input
initialValue={
props.monitorStepExternalStatusPageMonitor.retries?.toString() ||
"3"
}
placeholder="3"
type={InputType.NUMBER}
onChange={(value: string) => {
props.onChange({
...props.monitorStepExternalStatusPageMonitor,
retries: parseInt(value) || 3,
});
}}
/>
</div>
</div>
)}
</div>
);
};
export default ExternalStatusPageMonitorStepForm;

View File

@@ -83,6 +83,10 @@ import DomainMonitorStepForm from "./DomainMonitor/DomainMonitorStepForm";
import MonitorStepDomainMonitor, {
MonitorStepDomainMonitorUtil,
} from "Common/Types/Monitor/MonitorStepDomainMonitor";
import ExternalStatusPageMonitorStepForm from "./ExternalStatusPageMonitor/ExternalStatusPageMonitorStepForm";
import MonitorStepExternalStatusPageMonitor, {
MonitorStepExternalStatusPageMonitorUtil,
} from "Common/Types/Monitor/MonitorStepExternalStatusPageMonitor";
export interface ComponentProps {
monitorStatusDropdownOptions: Array<DropdownOption>;
@@ -834,6 +838,24 @@ return {
</Card>
)}
{props.monitorType === MonitorType.ExternalStatusPage && (
<Card
title="External Status Page Configuration"
description="Configure which external status page to monitor (e.g. AWS, GCP, GitHub)"
>
<ExternalStatusPageMonitorStepForm
monitorStepExternalStatusPageMonitor={
monitorStep.data?.externalStatusPageMonitor ||
MonitorStepExternalStatusPageMonitorUtil.getDefault()
}
onChange={(value: MonitorStepExternalStatusPageMonitor) => {
monitorStep.setExternalStatusPageMonitor(value);
props.onChange?.(MonitorStep.clone(monitorStep));
}}
/>
</Card>
)}
{/* Code Monitor Section */}
{isCodeMonitor && (
<Card

View File

@@ -324,6 +324,47 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
},
},
];
} else if (props.monitorType === MonitorType.ExternalStatusPage) {
fields = [
{
key: "externalStatusPageMonitor",
title: "Status Page URL",
description: "The URL of the external status page being monitored.",
fieldType: FieldType.Element,
placeholder: "No data entered",
getElement: (item: MonitorStepType): ReactElement => {
const externalStatusPageMonitor: any = item.externalStatusPageMonitor;
return <p>{externalStatusPageMonitor?.statusPageUrl || "-"}</p>;
},
},
{
key: "externalStatusPageMonitor",
title: "Provider",
description: "The provider type for this status page.",
fieldType: FieldType.Element,
placeholder: "Auto",
getElement: (item: MonitorStepType): ReactElement => {
const externalStatusPageMonitor: any = item.externalStatusPageMonitor;
return <p>{externalStatusPageMonitor?.provider || "Auto"}</p>;
},
},
{
key: "externalStatusPageMonitor",
title: "Component Name Filter",
description:
"If set, only this specific component will be monitored.",
fieldType: FieldType.Element,
placeholder: "All components",
getElement: (item: MonitorStepType): ReactElement => {
const externalStatusPageMonitor: any = item.externalStatusPageMonitor;
return (
<p>
{externalStatusPageMonitor?.componentName || "All components"}
</p>
);
},
},
];
} else if (props.monitorType === MonitorType.Logs) {
logFields = [];

View File

@@ -0,0 +1,136 @@
import OneUptimeDate from "Common/Types/Date";
import ProbeMonitorResponse from "Common/Types/Probe/ProbeMonitorResponse";
import ExternalStatusPageMonitorResponse, {
ExternalStatusPageComponentStatus,
} from "Common/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps {
probeMonitorResponse: ProbeMonitorResponse;
probeName?: string | undefined;
}
const ExternalStatusPageMonitorView: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const externalStatusPageResponse:
| ExternalStatusPageMonitorResponse
| undefined = props.probeMonitorResponse?.externalStatusPageResponse;
let responseTimeInMs: number =
externalStatusPageResponse?.responseTimeInMs || 0;
if (responseTimeInMs > 0) {
responseTimeInMs = Math.round(responseTimeInMs);
}
return (
<div className="space-y-5">
<div className="flex space-x-3">
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Probe"
value={props.probeName || "-"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Status"
value={props.probeMonitorResponse.isOnline ? "Online" : "Offline"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Overall Status"
value={externalStatusPageResponse?.overallStatus || "-"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Response Time"
value={responseTimeInMs ? responseTimeInMs + " ms" : "-"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Monitored At"
value={
props.probeMonitorResponse?.monitoredAt
? OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
props.probeMonitorResponse.monitoredAt,
)
: "-"
}
/>
</div>
<div className="flex space-x-3">
<InfoCard
className="w-1/3 shadow-none border-2 border-gray-100"
title="Active Incidents"
value={
externalStatusPageResponse?.activeIncidentCount?.toString() || "0"
}
/>
</div>
{props.probeMonitorResponse.failureCause && (
<div className="flex space-x-3">
<InfoCard
className="w-full shadow-none border-2 border-gray-100"
title="Error"
value={props.probeMonitorResponse.failureCause?.toString() || "-"}
/>
</div>
)}
{/* Component Statuses Section */}
{externalStatusPageResponse?.componentStatuses &&
externalStatusPageResponse.componentStatuses.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">
Component Statuses
</h3>
<div className="border rounded-md overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Component
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{externalStatusPageResponse.componentStatuses.map(
(
component: ExternalStatusPageComponentStatus,
index: number,
) => {
return (
<tr key={index}>
<td className="px-4 py-2 text-sm text-gray-900">
{component.name}
</td>
<td className="px-4 py-2 text-sm text-gray-500 font-mono">
{component.status}
</td>
<td className="px-4 py-2 text-sm text-gray-500">
{component.description || "-"}
</td>
</tr>
);
},
)}
</tbody>
</table>
</div>
</div>
)}
</div>
);
};
export default ExternalStatusPageMonitorView;

View File

@@ -9,6 +9,7 @@ import WebsiteMonitorSummaryView from "./WebsiteMonitorView";
import SnmpMonitorView from "./SnmpMonitorView";
import DnsMonitorView from "./DnsMonitorView";
import DomainMonitorView from "./DomainMonitorView";
import ExternalStatusPageMonitorView from "./ExternalStatusPageMonitorView";
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
import IncomingEmailMonitorRequest from "Common/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest";
import MonitorType, {
@@ -141,6 +142,15 @@ const SummaryInfo: FunctionComponent<ComponentProps> = (
);
}
if (props.monitorType === MonitorType.ExternalStatusPage) {
summaryComponent = (
<ExternalStatusPageMonitorView
probeMonitorResponse={probeMonitorResponse}
probeName={props.probeName}
/>
);
}
return (
<div key={key} className="space-y-6">
{summaryComponent}

View File

@@ -307,6 +307,18 @@ export default class CriteriaFilterUtil {
});
}
if (monitorType === MonitorType.ExternalStatusPage) {
options = options.filter((i: DropdownOption) => {
return (
i.value === CheckOn.ExternalStatusPageIsOnline ||
i.value === CheckOn.ExternalStatusPageOverallStatus ||
i.value === CheckOn.ExternalStatusPageComponentStatus ||
i.value === CheckOn.ExternalStatusPageActiveIncidents ||
i.value === CheckOn.ExternalStatusPageResponseTime
);
});
}
return options;
}
@@ -616,6 +628,42 @@ export default class CriteriaFilterUtil {
});
}
if (checkOn === CheckOn.ExternalStatusPageIsOnline) {
options = options.filter((i: DropdownOption) => {
return i.value === FilterType.True || i.value === FilterType.False;
});
}
if (
checkOn === CheckOn.ExternalStatusPageResponseTime ||
checkOn === CheckOn.ExternalStatusPageActiveIncidents
) {
options = options.filter((i: DropdownOption) => {
return (
i.value === FilterType.GreaterThan ||
i.value === FilterType.LessThan ||
i.value === FilterType.LessThanOrEqualTo ||
i.value === FilterType.GreaterThanOrEqualTo
);
});
}
if (
checkOn === CheckOn.ExternalStatusPageOverallStatus ||
checkOn === CheckOn.ExternalStatusPageComponentStatus
) {
options = options.filter((i: DropdownOption) => {
return (
i.value === FilterType.Contains ||
i.value === FilterType.NotContains ||
i.value === FilterType.EqualTo ||
i.value === FilterType.NotEqualTo ||
i.value === FilterType.StartsWith ||
i.value === FilterType.EndsWith
);
});
}
return options;
}
@@ -771,6 +819,22 @@ export default class CriteriaFilterUtil {
return "clientTransferProhibited";
}
if (checkOn === CheckOn.ExternalStatusPageResponseTime) {
return "5000";
}
if (checkOn === CheckOn.ExternalStatusPageOverallStatus) {
return "operational";
}
if (checkOn === CheckOn.ExternalStatusPageComponentStatus) {
return "operational";
}
if (checkOn === CheckOn.ExternalStatusPageActiveIncidents) {
return "0";
}
return "";
}
}

View File

@@ -0,0 +1,127 @@
# External Status Page Monitor
External Status Page monitoring allows you to monitor third-party status pages and get alerted when services you depend on experience outages or degraded performance. OneUptime periodically checks external status pages (such as AWS, GCP, Azure, GitHub, and more) and evaluates their status.
## Overview
External Status Page monitors check the health of services you rely on by querying their public status pages. This enables you to:
- Monitor the availability of third-party services your application depends on
- Get alerted when upstream providers experience outages
- Track individual component statuses (e.g., "AWS EC2 us-east-1")
- Detect degraded performance before it impacts your users
- Correlate your own incidents with upstream provider issues
## Supported Providers
OneUptime supports monitoring status pages via the following methods:
| Provider Type | Description |
|---|---|
| **Auto** (default) | Automatically detects the status page format |
| **Atlassian Statuspage** | Status pages powered by Atlassian Statuspage (JSON API) |
| **RSS** | Status pages that provide an RSS feed |
| **Atom** | Status pages that provide an Atom feed |
### Auto-Detection
When set to **Auto**, OneUptime will attempt to detect the status page format automatically:
1. First, it tries the Atlassian Statuspage JSON API (`/api/v2/status.json` and `/api/v2/components.json`)
2. If that fails, it attempts to parse the page as an RSS or Atom feed
3. As a final fallback, it performs a basic HTTP reachability check
## Creating an External Status Page Monitor
1. Go to **Monitors** in the OneUptime Dashboard
2. Click **Create Monitor**
3. Select **External Status Page** as the monitor type
4. Enter the status page URL you want to monitor
5. Optionally select a specific provider type (or leave as Auto)
6. Optionally enter a component name to filter monitoring to a specific component
7. Configure monitoring criteria as needed
## Configuration Options
### Status Page URL
Enter the URL of the external status page you want to monitor. For Atlassian Statuspage-powered sites, this is typically the root URL (e.g., `https://status.example.com`). For RSS/Atom feeds, enter the feed URL directly.
### Provider Type
Select the provider type for the status page. Use **Auto** (default) to let OneUptime detect the format automatically, or specify a specific provider type if you know it.
### Component Name Filter
If the status page reports on multiple components, you can optionally specify a component name to monitor only that specific component. For example, to monitor only AWS EC2 in us-east-1, you would enter `EC2 us-east-1` (the exact component name as shown on the status page).
When no component name is specified, the overall status of the status page is monitored.
### Advanced Options
#### Timeout
The maximum time (in milliseconds) to wait for a response from the status page. Default is 10000ms (10 seconds).
#### Retries
The number of times to retry the request if it fails. Default is 3 retries.
## Monitoring Criteria
You can configure criteria to determine when the external service is considered online, degraded, or offline based on:
- **Is Online** Whether the status page is reachable and returning status data
- **Overall Status** The overall status indicator of the status page (e.g., "operational", "major_outage")
- **Component Status** The status of a specific component (when using component name filter)
- **Active Incidents** The number of currently active incidents reported on the status page
- **Response Time** How long it takes to fetch the status page data
## Popular Status Page URLs
Here is a curated list of popular service status page URLs you can monitor:
| Service | Status Page URL |
|---|---|
| AWS | `https://health.aws.amazon.com/health/status` |
| Google Cloud Platform | `https://status.cloud.google.com` |
| Microsoft Azure | `https://status.azure.com` |
| GitHub | `https://www.githubstatus.com` |
| Cloudflare | `https://www.cloudflarestatus.com` |
| Datadog | `https://status.datadoghq.com` |
| PagerDuty | `https://status.pagerduty.com` |
| Twilio | `https://status.twilio.com` |
| Stripe | `https://status.stripe.com` |
| Slack | `https://status.slack.com` |
| Atlassian (Jira, Confluence) | `https://status.atlassian.com` |
| Vercel | `https://www.vercel-status.com` |
| Netlify | `https://www.netlifystatus.com` |
| DigitalOcean | `https://status.digitalocean.com` |
| Heroku | `https://status.heroku.com` |
| MongoDB Atlas | `https://status.cloud.mongodb.com` |
| Fastly | `https://status.fastly.com` |
| New Relic | `https://status.newrelic.com` |
| Sentry | `https://status.sentry.io` |
| CircleCI | `https://status.circleci.com` |
> **Note:** Many of these use Atlassian Statuspage, so the **Auto** provider type will detect them automatically.
## Incident & Alert Templating
When creating incidents or alerts from External Status Page monitors, you can use the following template variables:
| Variable | Description |
|---|---|
| `{{isOnline}}` | Whether the status page is online (true/false) |
| `{{responseTimeInMs}}` | Response time in milliseconds |
| `{{failureCause}}` | Reason for failure, if any |
| `{{overallStatus}}` | The overall status indicator value |
| `{{activeIncidentCount}}` | Number of active incidents |
| `{{componentStatuses}}` | JSON array of component statuses |
## Best Practices
- **Use Auto provider type** unless you know the exact format — Auto detection works well for most status pages
- **Monitor specific components** if you only depend on certain services (e.g., a specific AWS region)
- **Set up incident correlation** — when your monitors detect issues and the upstream status page also shows problems, it helps identify root causes faster
- **Combine with other monitors** — pair External Status Page monitors with your own API/Website monitors for comprehensive visibility

View File

@@ -182,6 +182,10 @@ const DocsNav: NavGroup[] = [
title: "Incoming Email Monitor",
url: "/docs/monitor/incoming-email-monitor",
},
{
title: "External Status Page Monitor",
url: "/docs/monitor/external-status-page-monitor",
},
{
title: "JavaScript Expressions",
url: "/docs/monitor/javascript-expression",

View File

@@ -19,6 +19,9 @@ import MonitorStepDnsMonitor from "Common/Types/Monitor/MonitorStepDnsMonitor";
import DomainMonitorUtil from "./MonitorTypes/DomainMonitor";
import DomainMonitorResponse from "Common/Types/Monitor/DomainMonitor/DomainMonitorResponse";
import MonitorStepDomainMonitor from "Common/Types/Monitor/MonitorStepDomainMonitor";
import ExternalStatusPageMonitorUtil from "./MonitorTypes/ExternalStatusPageMonitor";
import ExternalStatusPageMonitorResponse from "Common/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
import MonitorStepExternalStatusPageMonitor from "Common/Types/Monitor/MonitorStepExternalStatusPageMonitor";
import HTTPMethod from "Common/Types/API/HTTPMethod";
import URL from "Common/Types/API/URL";
import OneUptimeDate from "Common/Types/Date";
@@ -581,6 +584,39 @@ export default class MonitorUtil {
result.domainResponse = response;
}
if (monitorType === MonitorType.ExternalStatusPage) {
if (!monitorStep.data?.externalStatusPageMonitor) {
result.failureCause =
"External status page configuration not specified";
return result;
}
const externalStatusPageConfig: MonitorStepExternalStatusPageMonitor =
monitorStep.data.externalStatusPageMonitor;
if (!externalStatusPageConfig.statusPageUrl) {
result.failureCause = "Status page URL not specified";
return result;
}
const response: ExternalStatusPageMonitorResponse | null =
await ExternalStatusPageMonitorUtil.fetch(externalStatusPageConfig, {
retry: PROBE_MONITOR_RETRY_LIMIT,
monitorId: monitorId,
timeout: externalStatusPageConfig.timeout || 10000,
});
if (!response) {
return null;
}
result.isOnline = response.isOnline;
result.isTimeout = response.isTimeout;
result.responseTimeInMs = response.responseTimeInMs;
result.failureCause = response.failureCause;
result.externalStatusPageResponse = response;
}
// update the monitoredAt time to the current time.
result.monitoredAt = OneUptimeDate.getCurrentDate();

View File

@@ -0,0 +1,507 @@
import OnlineCheck from "../../OnlineCheck";
import logger from "Common/Server/Utils/Logger";
import ObjectID from "Common/Types/ObjectID";
import Sleep from "Common/Types/Sleep";
import MonitorStepExternalStatusPageMonitor from "Common/Types/Monitor/MonitorStepExternalStatusPageMonitor";
import ExternalStatusPageMonitorResponse, {
ExternalStatusPageComponentStatus,
} from "Common/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
import ExternalStatusPageProviderType from "Common/Types/Monitor/ExternalStatusPageProviderType";
import axios, { AxiosResponse } from "axios";
import { XMLParser } from "fast-xml-parser";
export interface ExternalStatusPageQueryOptions {
timeout?: number | undefined;
retry?: number | undefined;
currentRetryCount?: number | undefined;
monitorId?: ObjectID | undefined;
isOnlineCheckRequest?: boolean | undefined;
}
interface AtlassianStatusResponse {
page?: {
id?: string;
name?: string;
url?: string;
};
status?: {
indicator?: string;
description?: string;
};
}
interface AtlassianComponent {
id?: string;
name?: string;
status?: string;
description?: string;
group_id?: string | null;
}
interface AtlassianComponentsResponse {
components?: Array<AtlassianComponent>;
}
export default class ExternalStatusPageMonitorUtil {
public static async fetch(
config: MonitorStepExternalStatusPageMonitor,
options?: ExternalStatusPageQueryOptions,
): Promise<ExternalStatusPageMonitorResponse | null> {
if (!options) {
options = {};
}
if (options?.currentRetryCount === undefined) {
options.currentRetryCount = 1;
}
logger.debug(
`External Status Page Query: ${options?.monitorId?.toString()} ${config.statusPageUrl} - Retry: ${options?.currentRetryCount}`,
);
const startTime: [number, number] = process.hrtime();
try {
let response: ExternalStatusPageMonitorResponse | null = null;
const provider: ExternalStatusPageProviderType = config.provider;
if (provider === ExternalStatusPageProviderType.Auto) {
// Auto-detect: try Atlassian first, then fall back to RSS/Atom
response = await ExternalStatusPageMonitorUtil.tryAtlassianStatuspage(
config,
options,
);
if (!response) {
response = await ExternalStatusPageMonitorUtil.tryRssAtomFeed(
config,
options,
);
}
} else if (
provider === ExternalStatusPageProviderType.AtlassianStatuspage
) {
response = await ExternalStatusPageMonitorUtil.tryAtlassianStatuspage(
config,
options,
);
} else if (
provider === ExternalStatusPageProviderType.RSS ||
provider === ExternalStatusPageProviderType.Atom
) {
response = await ExternalStatusPageMonitorUtil.tryRssAtomFeed(
config,
options,
);
}
if (!response) {
// If all methods fail, just check if the URL is reachable
response = await ExternalStatusPageMonitorUtil.tryBasicHttpCheck(
config,
options,
);
}
const endTime: [number, number] = process.hrtime(startTime);
const responseTimeInMs: number = Math.ceil(
(endTime[0] * 1000000000 + endTime[1]) / 1000000,
);
if (response) {
response.responseTimeInMs = responseTimeInMs;
// Filter by component name if specified
if (config.componentName && response.componentStatuses.length > 0) {
const filterName: string = config.componentName.toLowerCase();
response.componentStatuses = response.componentStatuses.filter(
(c: ExternalStatusPageComponentStatus) => {
return c.name.toLowerCase().includes(filterName);
},
);
}
}
logger.debug(
`External Status Page Query success: ${options?.monitorId?.toString()} ${config.statusPageUrl} - Response Time: ${responseTimeInMs}ms`,
);
return response;
} catch (err: unknown) {
logger.debug(
`External Status Page Query error: ${options?.monitorId?.toString()} ${config.statusPageUrl}`,
);
logger.debug(err);
if (!options) {
options = {};
}
if (!options.currentRetryCount) {
options.currentRetryCount = 0;
}
if (options.currentRetryCount < (options.retry || config.retries || 3)) {
options.currentRetryCount++;
await Sleep.sleep(1000);
return await ExternalStatusPageMonitorUtil.fetch(config, options);
}
// Check if the probe is online
if (!options.isOnlineCheckRequest) {
if (!(await OnlineCheck.canProbeMonitorWebsiteMonitors())) {
logger.error(
`ExternalStatusPageMonitor - Probe is not online. Cannot fetch ${options?.monitorId?.toString()} ${config.statusPageUrl} - ERROR: ${err}`,
);
return null;
}
}
const endTime: [number, number] = process.hrtime(startTime);
const responseTimeInMs: number = Math.ceil(
(endTime[0] * 1000000000 + endTime[1]) / 1000000,
);
const isTimeout: boolean =
(err as Error).message?.toLowerCase().includes("timeout") ||
(err as Error).message?.toLowerCase().includes("timed out") ||
(err as Error).message?.toLowerCase().includes("etimeout") ||
(err as Error).message?.toLowerCase().includes("econnaborted");
if (isTimeout) {
return {
isOnline: false,
isTimeout: true,
overallStatus: "unknown",
componentStatuses: [],
activeIncidentCount: 0,
responseTimeInMs: responseTimeInMs,
failureCause:
"Request was tried " +
options.currentRetryCount +
" times and it timed out.",
};
}
return {
isOnline: false,
isTimeout: false,
overallStatus: "unknown",
componentStatuses: [],
activeIncidentCount: 0,
responseTimeInMs: responseTimeInMs,
failureCause: (err as Error).message || (err as Error).toString(),
};
}
}
private static async tryAtlassianStatuspage(
config: MonitorStepExternalStatusPageMonitor,
options: ExternalStatusPageQueryOptions,
): Promise<ExternalStatusPageMonitorResponse | null> {
try {
let baseUrl: string = config.statusPageUrl.replace(/\/+$/, "");
// Fetch status
const statusUrl: string = `${baseUrl}/api/v2/status.json`;
const statusResponse: AxiosResponse = await axios.get(statusUrl, {
timeout: config.timeout || options.timeout || 10000,
headers: {
Accept: "application/json",
"User-Agent": "OneUptime-Probe/1.0",
},
validateStatus: (status: number) => {
return status < 500;
},
});
if (
statusResponse.status === 404 ||
statusResponse.status === 403 ||
statusResponse.status === 401
) {
return null;
}
const statusData: AtlassianStatusResponse =
statusResponse.data as AtlassianStatusResponse;
if (!statusData?.status?.indicator) {
return null;
}
// Fetch components
const componentStatuses: Array<ExternalStatusPageComponentStatus> = [];
try {
const componentsUrl: string = `${baseUrl}/api/v2/components.json`;
const componentsResponse: AxiosResponse = await axios.get(
componentsUrl,
{
timeout: config.timeout || options.timeout || 10000,
headers: {
Accept: "application/json",
"User-Agent": "OneUptime-Probe/1.0",
},
},
);
const componentsData: AtlassianComponentsResponse =
componentsResponse.data as AtlassianComponentsResponse;
if (componentsData?.components) {
for (const component of componentsData.components) {
// Skip group headers (components with no group_id that are groups themselves)
if (component.name && component.status) {
componentStatuses.push({
name: component.name,
status: component.status,
description: component.description || undefined,
});
}
}
}
} catch (err) {
logger.debug(
`Failed to fetch Atlassian components for ${baseUrl}: ${err}`,
);
// Continue without component data
}
const overallStatus: string =
statusData.status.description || statusData.status.indicator || "";
return {
isOnline: true,
overallStatus: overallStatus,
componentStatuses: componentStatuses,
activeIncidentCount: 0, // Could be enhanced later with /api/v2/incidents/unresolved.json
responseTimeInMs: 0, // Will be overwritten by caller
failureCause: "",
rawBody: JSON.stringify(statusData),
};
} catch (err) {
logger.debug(
`Atlassian Statuspage API failed for ${config.statusPageUrl}: ${err}`,
);
return null;
}
}
private static async tryRssAtomFeed(
config: MonitorStepExternalStatusPageMonitor,
options: ExternalStatusPageQueryOptions,
): Promise<ExternalStatusPageMonitorResponse | null> {
try {
const feedUrl: string = config.statusPageUrl.replace(/\/+$/, "");
const response: AxiosResponse = await axios.get(feedUrl, {
timeout: config.timeout || options.timeout || 10000,
headers: {
Accept:
"application/rss+xml, application/atom+xml, application/xml, text/xml",
"User-Agent": "OneUptime-Probe/1.0",
},
responseType: "text",
});
const body: string = response.data as string;
if (!body || typeof body !== "string") {
return null;
}
// Check if it looks like XML
const trimmed: string = body.trim();
if (!trimmed.startsWith("<")) {
return null;
}
const parser: XMLParser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
});
const parsed: Record<string, unknown> = parser.parse(body) as Record<
string,
unknown
>;
// Try RSS format
if (parsed["rss"]) {
return ExternalStatusPageMonitorUtil.parseRssFeed(
parsed["rss"] as Record<string, unknown>,
body,
);
}
// Try Atom format
if (parsed["feed"]) {
return ExternalStatusPageMonitorUtil.parseAtomFeed(
parsed["feed"] as Record<string, unknown>,
body,
);
}
return null;
} catch (err) {
logger.debug(
`RSS/Atom feed parsing failed for ${config.statusPageUrl}: ${err}`,
);
return null;
}
}
private static parseRssFeed(
rss: Record<string, unknown>,
rawBody: string,
): ExternalStatusPageMonitorResponse {
const channel: Record<string, unknown> =
(rss["channel"] as Record<string, unknown>) || {};
const items: Array<Record<string, unknown>> = Array.isArray(
channel["item"],
)
? (channel["item"] as Array<Record<string, unknown>>)
: channel["item"]
? [channel["item"] as Record<string, unknown>]
: [];
// Count active incidents (recent items — items published in the last 24 hours)
const now: Date = new Date();
let activeIncidentCount: number = 0;
const componentStatuses: Array<ExternalStatusPageComponentStatus> = [];
for (const item of items) {
const pubDate: string = (item["pubDate"] as string) || "";
if (pubDate) {
const itemDate: Date = new Date(pubDate);
const hoursDiff: number =
(now.getTime() - itemDate.getTime()) / (1000 * 60 * 60);
if (hoursDiff <= 24) {
activeIncidentCount++;
componentStatuses.push({
name: (item["title"] as string) || "Unknown",
status: "incident",
description: (item["description"] as string) || undefined,
});
}
}
}
const overallStatus: string =
activeIncidentCount > 0 ? "degraded" : "operational";
return {
isOnline: true,
overallStatus: overallStatus,
componentStatuses: componentStatuses,
activeIncidentCount: activeIncidentCount,
responseTimeInMs: 0,
failureCause: "",
rawBody: rawBody,
};
}
private static parseAtomFeed(
feed: Record<string, unknown>,
rawBody: string,
): ExternalStatusPageMonitorResponse {
const entries: Array<Record<string, unknown>> = Array.isArray(
feed["entry"],
)
? (feed["entry"] as Array<Record<string, unknown>>)
: feed["entry"]
? [feed["entry"] as Record<string, unknown>]
: [];
// Count active incidents (recent entries — entries updated in the last 24 hours)
const now: Date = new Date();
let activeIncidentCount: number = 0;
const componentStatuses: Array<ExternalStatusPageComponentStatus> = [];
for (const entry of entries) {
const updated: string =
(entry["updated"] as string) || (entry["published"] as string) || "";
if (updated) {
const entryDate: Date = new Date(updated);
const hoursDiff: number =
(now.getTime() - entryDate.getTime()) / (1000 * 60 * 60);
if (hoursDiff <= 24) {
activeIncidentCount++;
const title: string | Record<string, unknown> =
(entry["title"] as string | Record<string, unknown>) || "Unknown";
const titleText: string =
typeof title === "string"
? title
: (title["#text"] as string) || "Unknown";
const content: string | Record<string, unknown> =
(entry["content"] as string | Record<string, unknown>) ||
(entry["summary"] as string | Record<string, unknown>) ||
"";
const contentText: string =
typeof content === "string"
? content
: (content["#text"] as string) || "";
componentStatuses.push({
name: titleText,
status: "incident",
description: contentText || undefined,
});
}
}
}
const overallStatus: string =
activeIncidentCount > 0 ? "degraded" : "operational";
return {
isOnline: true,
overallStatus: overallStatus,
componentStatuses: componentStatuses,
activeIncidentCount: activeIncidentCount,
responseTimeInMs: 0,
failureCause: "",
rawBody: rawBody,
};
}
private static async tryBasicHttpCheck(
config: MonitorStepExternalStatusPageMonitor,
options: ExternalStatusPageQueryOptions,
): Promise<ExternalStatusPageMonitorResponse> {
try {
const response: AxiosResponse = await axios.get(config.statusPageUrl, {
timeout: config.timeout || options.timeout || 10000,
headers: {
"User-Agent": "OneUptime-Probe/1.0",
},
validateStatus: () => {
return true;
},
});
const isOnline: boolean = response.status >= 200 && response.status < 400;
return {
isOnline: isOnline,
overallStatus: isOnline ? "reachable" : "unreachable",
componentStatuses: [],
activeIncidentCount: 0,
responseTimeInMs: 0,
failureCause: isOnline
? ""
: `HTTP status ${response.status}`,
};
} catch (err) {
return {
isOnline: false,
overallStatus: "unreachable",
componentStatuses: [],
activeIncidentCount: 0,
responseTimeInMs: 0,
failureCause: (err as Error).message || (err as Error).toString(),
};
}
}
}

View File

@@ -25,6 +25,7 @@
"axios": "^1.13.1",
"Common": "file:../Common",
"ejs": "^3.1.10",
"fast-xml-parser": "^5.2.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.5",
"net-snmp": "^3.26.1",

View File

@@ -257,6 +257,31 @@ export default class MonitorUtil {
}
}
if (monitorType === MonitorType.ExternalStatusPage) {
for (const monitorStep of monitorSteps?.data?.monitorStepsInstanceArray ||
[]) {
// Handle External Status Page URL secrets
if (
monitorStep.data?.externalStatusPageMonitor?.statusPageUrl &&
this.hasSecrets(
monitorStep.data.externalStatusPageMonitor.statusPageUrl,
)
) {
if (!isSecretsLoaded) {
monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId);
isSecretsLoaded = true;
}
monitorStep.data.externalStatusPageMonitor.statusPageUrl =
(await MonitorUtil.fillSecretsInStringOrJSON({
secrets: monitorSecrets,
populateSecretsIn:
monitorStep.data.externalStatusPageMonitor.statusPageUrl,
})) as string;
}
}
}
return monitorSteps;
}