mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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 || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
8
Common/Types/Monitor/ExternalStatusPageProviderType.ts
Normal file
8
Common/Types/Monitor/ExternalStatusPageProviderType.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
enum ExternalStatusPageProviderType {
|
||||
AtlassianStatuspage = "Atlassian Statuspage",
|
||||
RSS = "RSS",
|
||||
Atom = "Atom",
|
||||
Auto = "Auto",
|
||||
}
|
||||
|
||||
export default ExternalStatusPageProviderType;
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
48
Common/Types/Monitor/MonitorStepExternalStatusPageMonitor.ts
Normal file
48
Common/Types/Monitor/MonitorStepExternalStatusPageMonitor.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
507
Probe/Utils/Monitors/MonitorTypes/ExternalStatusPageMonitor.ts
Normal file
507
Probe/Utils/Monitors/MonitorTypes/ExternalStatusPageMonitor.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user